Repository: Appboy/appboy-ios-sdk Branch: master Commit: 2afdd54b1718 Files: 582 Total size: 1.8 MB Directory structure: gitextract_0k488zaf/ ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── bug.yml │ ├── config.yml │ └── feature.yml ├── .gitignore ├── Appboy-Push-Story.podspec ├── Appboy-iOS-SDK.podspec ├── Appboy-tvOS-SDK/ │ └── AppboyTVOSKit.framework/ │ ├── AppboyTVOSKit │ ├── Headers/ │ │ ├── ABKAttributionData.h │ │ ├── ABKBannerCard.h │ │ ├── ABKBannerContentCard.h │ │ ├── ABKCaptionedImageCard.h │ │ ├── ABKCaptionedImageContentCard.h │ │ ├── ABKCard.h │ │ ├── ABKClassicCard.h │ │ ├── ABKClassicContentCard.h │ │ ├── ABKContentCard.h │ │ ├── ABKFacebookUser.h │ │ ├── ABKFeedController.h │ │ ├── ABKSdkAuthenticationDelegate.h │ │ ├── ABKSdkAuthenticationError.h │ │ ├── ABKSdkMetadata.h │ │ ├── ABKTextAnnouncementCard.h │ │ ├── ABKTwitterUser.h │ │ ├── ABKUser.h │ │ ├── Appboy.h │ │ └── AppboyKit.h │ ├── Info.plist │ └── Modules/ │ └── module.modulemap ├── Appboy-tvOS-SDK.podspec ├── AppboyKit/ │ ├── ABKLocationManagerProvider.m │ ├── ABKModalWebViewController.m │ ├── ABKNoConnectionLocalization.m │ ├── ABKSDWebImageProxy.m │ ├── Appboy.bundle/ │ │ ├── Info.plist │ │ ├── PrivacyInfo.xcprivacy │ │ ├── ZipArchive_LICENSE.txt │ │ ├── _CodeSignature/ │ │ │ ├── CodeDirectory │ │ │ ├── CodeRequirements │ │ │ ├── CodeRequirements-1 │ │ │ ├── CodeResources │ │ │ └── CodeSignature │ │ ├── appboy-spm-cleanup.sh │ │ ├── ar.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── cs.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── da.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── de.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── en.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── es-419.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── es-MX.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── es.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── et.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── fi.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── fil.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── fr.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── he.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── hi.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── id.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── it.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── ja.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── km.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── ko.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── lo.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── ms.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── my.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── nb.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── nl.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── pl.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── pt-PT.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── pt.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── ru.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── sv.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── th.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── uk.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── vi.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── zh-HK.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── zh-Hans.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── zh-Hant.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ ├── zh-TW.lproj/ │ │ │ └── LocalizedAppboyUIString.strings │ │ └── zh.lproj/ │ │ └── LocalizedAppboyUIString.strings │ └── include/ │ ├── ABKAttributionData.h │ ├── ABKBannerCard.h │ ├── ABKBannerContentCard.h │ ├── ABKCaptionedImageCard.h │ ├── ABKCaptionedImageContentCard.h │ ├── ABKCard.h │ ├── ABKClassicCard.h │ ├── ABKClassicContentCard.h │ ├── ABKContentCard.h │ ├── ABKContentCardsController.h │ ├── ABKFacebookUser.h │ ├── ABKFeedController.h │ ├── ABKIDFADelegate.h │ ├── ABKImageDelegate.h │ ├── ABKInAppMessage.h │ ├── ABKInAppMessageButton.h │ ├── ABKInAppMessageControl.h │ ├── ABKInAppMessageController.h │ ├── ABKInAppMessageControllerDelegate.h │ ├── ABKInAppMessageDarkButtonTheme.h │ ├── ABKInAppMessageDarkTheme.h │ ├── ABKInAppMessageFull.h │ ├── ABKInAppMessageHTML.h │ ├── ABKInAppMessageHTMLBase.h │ ├── ABKInAppMessageHTMLFull.h │ ├── ABKInAppMessageImmersive.h │ ├── ABKInAppMessageModal.h │ ├── ABKInAppMessageSlideup.h │ ├── ABKInAppMessageUIControlling.h │ ├── ABKInAppMessageWebViewBridge.h │ ├── ABKLocationManager.h │ ├── ABKLocationManagerProvider.h │ ├── ABKModalWebViewController.h │ ├── ABKNoConnectionLocalization.h │ ├── ABKPushUtils.h │ ├── ABKSDWebImageProxy.h │ ├── ABKSdkAuthenticationDelegate.h │ ├── ABKSdkAuthenticationError.h │ ├── ABKSdkMetadata.h │ ├── ABKTextAnnouncementCard.h │ ├── ABKTwitterUser.h │ ├── ABKURLDelegate.h │ ├── ABKUser.h │ ├── Appboy.h │ └── AppboyKit.h ├── AppboyPushStory/ │ ├── Dummy.m │ ├── Resources/ │ │ └── ABKPageView.nib │ └── include/ │ └── AppboyPushStory/ │ ├── ABKStoriesView.h │ ├── ABKStoriesViewDataSource.h │ └── AppboyPushStory.h ├── AppboyUI/ │ ├── ABKContentCards/ │ │ ├── AppboyContentCards.h │ │ ├── Resources/ │ │ │ ├── Base.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── ar.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── cs.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── da.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── de.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── en.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── es-419.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── es-MX.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── es.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── et.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── fi.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── fil.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── fr.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── he.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── hi.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── id.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── it.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── ja.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── km.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── ko.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── lo.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── ms.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── my.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── nb.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── nl.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── pl.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── pt-PT.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── pt.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── ru.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── sv.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── th.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── uk.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── vi.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── zh-HK.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── zh-Hans.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── zh-Hant.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ ├── zh-TW.lproj/ │ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ │ └── zh.lproj/ │ │ │ └── AppboyContentCardsLocalizable.strings │ │ └── ViewControllers/ │ │ ├── ABKContentCardsTableViewController.h │ │ ├── ABKContentCardsTableViewController.m │ │ ├── ABKContentCardsViewController.h │ │ ├── ABKContentCardsViewController.m │ │ ├── ABKContentCardsWebViewController.h │ │ ├── ABKContentCardsWebViewController.m │ │ └── Cells/ │ │ ├── ABKBannerContentCardCell.h │ │ ├── ABKBannerContentCardCell.m │ │ ├── ABKBaseContentCardCell.h │ │ ├── ABKBaseContentCardCell.m │ │ ├── ABKCaptionedImageContentCardCell.h │ │ ├── ABKCaptionedImageContentCardCell.m │ │ ├── ABKClassicContentCardCell.h │ │ ├── ABKClassicContentCardCell.m │ │ ├── ABKClassicImageContentCardCell.h │ │ ├── ABKClassicImageContentCardCell.m │ │ ├── ABKControlTableViewCell.h │ │ └── ABKControlTableViewCell.m │ ├── ABKInAppMessage/ │ │ ├── ABKInAppMessageUIButton.h │ │ ├── ABKInAppMessageUIButton.m │ │ ├── ABKInAppMessageUIController.h │ │ ├── ABKInAppMessageUIController.m │ │ ├── ABKInAppMessageUIDelegate.h │ │ ├── ABKInAppMessageView.h │ │ ├── ABKInAppMessageView.m │ │ ├── ABKInAppMessageWindow.h │ │ ├── ABKInAppMessageWindow.m │ │ ├── AppboyInAppMessage.h │ │ ├── Resources/ │ │ │ ├── ABKInAppMessageFullViewController.xib │ │ │ ├── ABKInAppMessageModalViewController.xib │ │ │ ├── ABKInAppMessageSlideupViewController.xib │ │ │ ├── Base.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── FontAwesome.otf │ │ │ ├── ar.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── cs.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── da.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── de.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── en.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── es-419.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── es-MX.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── es.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── et.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── fi.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── fil.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── fr.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── he.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── hi.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── id.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── it.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── ja.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── km.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── ko.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── lo.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── ms.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── my.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── nb.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── nl.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── pl.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── pt-PT.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── pt.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── ru.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── sv.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── th.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── uk.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── vi.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── zh-HK.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── zh-Hans.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── zh-Hant.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ ├── zh-TW.lproj/ │ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ │ └── zh.lproj/ │ │ │ └── AppboyInAppMessageLocalizable.strings │ │ └── ViewControllers/ │ │ ├── ABKInAppMessageFullViewController.h │ │ ├── ABKInAppMessageFullViewController.m │ │ ├── ABKInAppMessageHTMLBaseViewController.h │ │ ├── ABKInAppMessageHTMLBaseViewController.m │ │ ├── ABKInAppMessageHTMLFullViewController.h │ │ ├── ABKInAppMessageHTMLFullViewController.m │ │ ├── ABKInAppMessageHTMLViewController.h │ │ ├── ABKInAppMessageHTMLViewController.m │ │ ├── ABKInAppMessageImmersiveViewController.h │ │ ├── ABKInAppMessageImmersiveViewController.m │ │ ├── ABKInAppMessageModalViewController.h │ │ ├── ABKInAppMessageModalViewController.m │ │ ├── ABKInAppMessageSlideupViewController.h │ │ ├── ABKInAppMessageSlideupViewController.m │ │ ├── ABKInAppMessageViewController.h │ │ ├── ABKInAppMessageViewController.m │ │ ├── ABKInAppMessageWindowController.h │ │ └── ABKInAppMessageWindowController.m │ ├── ABKNewsFeed/ │ │ ├── AppboyNewsFeed.h │ │ ├── Resources/ │ │ │ ├── Base.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── ar.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── cs.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── da.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── de.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── en.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── es-419.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── es-MX.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── es.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── et.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── fi.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── fil.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── fr.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── he.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── hi.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── id.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── it.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── ja.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── km.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── ko.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── lo.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── ms.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── my.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── nb.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── nl.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── pl.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── pt-PT.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── pt.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── ru.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── sv.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── th.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── uk.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── vi.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── zh-HK.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── zh-Hans.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── zh-Hant.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ ├── zh-TW.lproj/ │ │ │ │ └── AppboyFeedLocalizable.strings │ │ │ └── zh.lproj/ │ │ │ └── AppboyFeedLocalizable.strings │ │ └── ViewControllers/ │ │ ├── ABKFeedWebViewController.h │ │ ├── ABKFeedWebViewController.m │ │ ├── ABKNewsFeedTableViewController.h │ │ ├── ABKNewsFeedTableViewController.m │ │ ├── ABKNewsFeedViewController.h │ │ ├── ABKNewsFeedViewController.m │ │ └── Cells/ │ │ ├── ABKNFBannerCardCell.h │ │ ├── ABKNFBannerCardCell.m │ │ ├── ABKNFBaseCardCell.h │ │ ├── ABKNFBaseCardCell.m │ │ ├── ABKNFCaptionedMessageCardCell.h │ │ ├── ABKNFCaptionedMessageCardCell.m │ │ ├── ABKNFClassicCardCell.h │ │ └── ABKNFClassicCardCell.m │ └── ABKUIUtils/ │ ├── ABKSDWebImageImageDelegate.h │ ├── ABKSDWebImageImageDelegate.m │ ├── ABKUIURLUtils.h │ ├── ABKUIURLUtils.m │ ├── ABKUIUtils.h │ └── ABKUIUtils.m ├── CHANGELOG.md ├── Example/ │ ├── Podfile │ ├── README.md │ ├── Stopwatch/ │ │ ├── Resources/ │ │ │ ├── ABKContentCardsCustomStoryboard.storyboard │ │ │ ├── ABKContentCardsStoryboard.storyboard │ │ │ ├── ABKNewsFeedCardStoryboard.storyboard │ │ │ ├── Assets.xcassets/ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── IAM.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Icons_Read.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Icons_Unread.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── appboy_cc_noimage_lrg.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── bolt.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── newsfeed.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── stopwatch_cc_icon_pinned.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── user.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── LaunchScreen.storyboard │ │ │ ├── en.lproj/ │ │ │ │ ├── Localizable.strings │ │ │ │ └── MainStoryboard.storyboard │ │ │ ├── he.lproj/ │ │ │ │ └── Localizable.strings │ │ │ ├── zh-Hans.lproj/ │ │ │ │ └── Localizable.strings │ │ │ └── zh-Hant.lproj/ │ │ │ └── Localizable.strings │ │ ├── Sources/ │ │ │ ├── AppDelegate.h │ │ │ ├── AppDelegate.m │ │ │ ├── Categories/ │ │ │ │ ├── UIViewController+Keyboard.h │ │ │ │ └── UIViewController+Keyboard.m │ │ │ ├── Utils/ │ │ │ │ ├── AlertControllerUtils.h │ │ │ │ ├── AlertControllerUtils.m │ │ │ │ ├── ColorUtils.h │ │ │ │ ├── ColorUtils.m │ │ │ │ ├── IDFADelegate.h │ │ │ │ ├── IDFADelegate.m │ │ │ │ ├── LoggerUtils.h │ │ │ │ ├── SdkAuthDelegate.h │ │ │ │ └── SdkAuthDelegate.m │ │ │ ├── ViewControllers/ │ │ │ │ ├── Advanced/ │ │ │ │ │ ├── About/ │ │ │ │ │ │ ├── AboutViewController.h │ │ │ │ │ │ └── AboutViewController.m │ │ │ │ │ ├── Data/ │ │ │ │ │ │ ├── DataViewController.h │ │ │ │ │ │ └── DataViewController.m │ │ │ │ │ └── Misc/ │ │ │ │ │ ├── CustomThemesDataSource.h │ │ │ │ │ ├── CustomThemesDataSource.m │ │ │ │ │ ├── GeofencesViewController.h │ │ │ │ │ ├── GeofencesViewController.m │ │ │ │ │ ├── MiscViewController.h │ │ │ │ │ └── MiscViewController.m │ │ │ │ ├── Braze UI/ │ │ │ │ │ ├── FeedUIViewController.h │ │ │ │ │ └── FeedUIViewController.m │ │ │ │ ├── ContainerViewController.h │ │ │ │ ├── ContainerViewController.m │ │ │ │ ├── CustomTabBarController.h │ │ │ │ ├── CustomTabBarController.m │ │ │ │ ├── IAM/ │ │ │ │ │ ├── Controls/ │ │ │ │ │ │ ├── InAppMessageTestViewController.h │ │ │ │ │ │ └── InAppMessageTestViewController.m │ │ │ │ │ └── UI/ │ │ │ │ │ ├── HTML/ │ │ │ │ │ │ ├── InAppMessageWithJS.html │ │ │ │ │ │ └── InAppMessageWithoutAssetZip.html │ │ │ │ │ ├── InAppMessageHTMLComposerViewController.h │ │ │ │ │ ├── InAppMessageHTMLComposerViewController.m │ │ │ │ │ ├── InAppMessageUICells.h │ │ │ │ │ ├── InAppMessageUICells.m │ │ │ │ │ ├── InAppMessageUIViewController.h │ │ │ │ │ └── InAppMessageUIViewController.m │ │ │ │ └── User/ │ │ │ │ ├── Alias/ │ │ │ │ │ ├── AliasViewController.h │ │ │ │ │ └── AliasViewController.m │ │ │ │ ├── Array/ │ │ │ │ │ ├── UserAttributesArrayViewController.h │ │ │ │ │ └── UserAttributesArrayViewController.m │ │ │ │ ├── Attributes/ │ │ │ │ │ ├── Location Custom Attribute/ │ │ │ │ │ │ ├── LocationAnnotation.h │ │ │ │ │ │ ├── LocationAnnotation.m │ │ │ │ │ │ ├── LocationCustomAttributeViewController.h │ │ │ │ │ │ └── LocationCustomAttributeViewController.m │ │ │ │ │ ├── UserAttributesViewController.h │ │ │ │ │ ├── UserAttributesViewController.m │ │ │ │ │ └── Views/ │ │ │ │ │ ├── UserCells.h │ │ │ │ │ ├── UserCells.m │ │ │ │ │ ├── UserCustomAttribute.h │ │ │ │ │ ├── UserCustomAttribute.m │ │ │ │ │ ├── UserCustomAttributeCell.h │ │ │ │ │ ├── UserCustomAttributeCell.m │ │ │ │ │ ├── UserSubscriptionGroup.h │ │ │ │ │ ├── UserSubscriptionGroup.m │ │ │ │ │ ├── UserSubscriptionGroupCell.h │ │ │ │ │ └── UserSubscriptionGroupCell.m │ │ │ │ └── Events/ │ │ │ │ ├── EventsViewController.h │ │ │ │ └── EventsViewController.m │ │ │ ├── Views/ │ │ │ │ ├── ScrollContentView.h │ │ │ │ ├── ScrollContentView.m │ │ │ │ ├── StopwatchButton.h │ │ │ │ ├── StopwatchButton.m │ │ │ │ ├── StopwatchSegmentedControl.h │ │ │ │ └── StopwatchSegmentedControl.m │ │ │ └── main.m │ │ └── Supporting Files/ │ │ ├── Info.plist │ │ ├── Stopwatch-Prefix.pch │ │ └── Stopwatch.entitlements │ ├── Stopwatch.xcodeproj/ │ │ ├── project.pbxproj │ │ └── project.xcworkspace/ │ │ └── contents.xcworkspacedata │ ├── Stopwatch.xcworkspace/ │ │ └── contents.xcworkspacedata │ ├── StopwatchNotificationContentExtension/ │ │ ├── Base.lproj/ │ │ │ └── MainInterface.storyboard │ │ ├── Info.plist │ │ ├── NotificationViewController.h │ │ ├── NotificationViewController.m │ │ └── StopwatchNotificationContentExtension.entitlements │ ├── StopwatchNotificationService/ │ │ ├── Info.plist │ │ ├── NotificationService.h │ │ └── NotificationService.m │ ├── tvOS_Stopwatch/ │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets/ │ │ │ ├── App Icon & Top Shelf Image.brandassets/ │ │ │ │ ├── App Icon - Large.imagestack/ │ │ │ │ │ ├── Back.imagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Front.imagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Middle.imagestacklayer/ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── App Icon - Small.imagestack/ │ │ │ │ │ ├── Back.imagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Front.imagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Middle.imagestacklayer/ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Top Shelf Image Wide.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── Top Shelf Image.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ └── LaunchImage.launchimage/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── ViewController.h │ │ ├── ViewController.m │ │ └── main.m │ └── tvOS_TVML_Stopwatch/ │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── AppboyBridge.h │ ├── AppboyBridge.m │ ├── Assets.xcassets/ │ │ ├── App Icon & Top Shelf Image.brandassets/ │ │ │ ├── App Icon - Large.imagestack/ │ │ │ │ ├── Back.imagestacklayer/ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Front.imagestacklayer/ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ └── Middle.imagestacklayer/ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── App Icon - Small.imagestack/ │ │ │ │ ├── Back.imagestacklayer/ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Front.imagestacklayer/ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ └── Middle.imagestacklayer/ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ └── Top Shelf Image.imageset/ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── LaunchImage.launchimage/ │ │ └── Contents.json │ ├── Info.plist │ ├── application.js │ └── main.m ├── HelloSwift/ │ ├── HelloSwift/ │ │ ├── AppDelegate.swift │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.xib │ │ │ └── Main.storyboard │ │ ├── HelloSwift-Bridging-Header.h │ │ ├── HelloSwift.entitlements │ │ ├── Images.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Info.plist │ │ └── ViewController.swift │ ├── HelloSwift.xcodeproj/ │ │ └── project.pbxproj │ ├── HelloSwift.xcworkspace/ │ │ └── contents.xcworkspacedata │ ├── HelloSwiftNotificationContentExtension/ │ │ ├── Base.lproj/ │ │ │ └── MainInterface.storyboard │ │ ├── HelloSwiftNotificationContentExtension.entitlements │ │ ├── Info.plist │ │ └── NotificationViewController.swift │ ├── HelloSwiftNotificationExtension/ │ │ ├── HelloSwiftNotificationExtension.entitlements │ │ ├── Info.plist │ │ └── NotificationService.swift │ ├── HelloSwiftTests/ │ │ └── AppboyPushUnitTests.m │ ├── Podfile │ └── tvOS_HelloSwift/ │ ├── AppDelegate.swift │ ├── Assets.xcassets/ │ │ ├── App Icon & Top Shelf Image.brandassets/ │ │ │ ├── App Icon - App Store.imagestack/ │ │ │ │ ├── Back.imagestacklayer/ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Front.imagestacklayer/ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ └── Middle.imagestacklayer/ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── App Icon.imagestack/ │ │ │ │ ├── Back.imagestacklayer/ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Front.imagestacklayer/ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ └── Middle.imagestacklayer/ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Top Shelf Image Wide.imageset/ │ │ │ │ └── Contents.json │ │ │ └── Top Shelf Image.imageset/ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj/ │ │ └── LaunchScreen.storyboard │ ├── ContentView.swift │ ├── Info.plist │ └── Preview Content/ │ └── Preview Assets.xcassets/ │ └── Contents.json ├── LICENSE ├── Package.swift ├── README.md ├── Samples/ │ ├── ContentCards/ │ │ └── BrazeContentCardsSampleApp/ │ │ ├── BrazeContentCardsSampleApp/ │ │ │ ├── AppDelegate.h │ │ │ ├── AppDelegate.m │ │ │ ├── Assets.xcassets/ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj/ │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── Main.storyboard │ │ │ ├── CustomCaptionedImageContentCardCell.h │ │ │ ├── CustomCaptionedImageContentCardCell.m │ │ │ ├── CustomClassicContentCardCell.h │ │ │ ├── CustomClassicContentCardCell.m │ │ │ ├── CustomContentCardsTableViewController.h │ │ │ ├── CustomContentCardsTableViewController.m │ │ │ ├── Info.plist │ │ │ ├── ViewController.h │ │ │ ├── ViewController.m │ │ │ └── main.m │ │ ├── BrazeContentCardsSampleApp.xcodeproj/ │ │ │ └── project.pbxproj │ │ ├── BrazeContentCardsSampleApp.xcworkspace/ │ │ │ └── contents.xcworkspacedata │ │ ├── Podfile │ │ └── Podfile.lock │ ├── Core/ │ │ └── ObjCSample/ │ │ ├── ObjCSample/ │ │ │ ├── AppDelegate.h │ │ │ ├── AppDelegate.m │ │ │ ├── Assets.xcassets/ │ │ │ │ └── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj/ │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── Main.storyboard │ │ │ ├── Info.plist │ │ │ ├── ObjCSample.entitlements │ │ │ ├── ViewController.h │ │ │ ├── ViewController.m │ │ │ └── main.m │ │ ├── ObjCSample.xcodeproj/ │ │ │ └── project.pbxproj │ │ ├── ObjCSample.xcworkspace/ │ │ │ └── contents.xcworkspacedata │ │ ├── Podfile │ │ └── Podfile.lock │ ├── InAppMessage/ │ │ └── BrazeInAppMessageSample/ │ │ ├── BrazeInAppMessageSample/ │ │ │ ├── AppDelegate.h │ │ │ ├── AppDelegate.m │ │ │ ├── Assets.xcassets/ │ │ │ │ └── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj/ │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── Main.storyboard │ │ │ ├── CustomInAppMessageViewController.h │ │ │ ├── CustomInAppMessageViewController.m │ │ │ ├── CustomInAppMessageViewController.xib │ │ │ ├── Info.plist │ │ │ ├── ViewController.h │ │ │ ├── ViewController.m │ │ │ └── main.m │ │ ├── BrazeInAppMessageSample.xcodeproj/ │ │ │ └── project.pbxproj │ │ ├── BrazeInAppMessageSample.xcworkspace/ │ │ │ └── contents.xcworkspacedata │ │ ├── Podfile │ │ └── Podfile.lock │ └── NewsFeed/ │ └── BrazeNewsFeedSample/ │ ├── BrazeNewsFeedSample/ │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── CustomFeedTableViewController.h │ │ ├── Info.plist │ │ ├── ViewController.h │ │ ├── ViewController.m │ │ └── main.m │ ├── BrazeNewsFeedSample.xcodeproj/ │ │ └── project.pbxproj │ ├── BrazeNewsFeedSample.xcworkspace/ │ │ └── contents.xcworkspacedata │ ├── Podfile │ └── Podfile.lock ├── appboy_ios_sdk.json ├── appboy_ios_sdk_core.json └── appboy_ios_sdk_full.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug.yml ================================================ 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. Consider upgrading to the new [Braze Swift SDK](https://github.com/braze-inc/braze-swift-sdk), the bug might not exist in this new SDK. - type: dropdown id: platform attributes: label: Platform multiple: false options: - iOS - tvOS - Mac Catalyst - Other validations: required: true - type: input id: platform_version attributes: label: Platform Version placeholder: ex. iOS 15.1 validations: required: true - type: input id: sdk_version attributes: label: Braze SDK Version placeholder: ex. 4.3.0 validations: required: true - type: input id: xcode_version attributes: label: Xcode Version placeholder: ex. Xcode 10.1 validations: required: true - type: dropdown id: integration_method attributes: label: Integration Method multiple: false options: - Swift Package Manager - Cocoapods - Carthage - Manually - Other 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 `pod 'Appboy-iOS-SDK'` to the Podspec file. 2. Add `[Appboy startWithApiKey:inApplication:withLaunchOptions:];` method in `application:didFinishLaunchingWithOptions:` method in `AppDelegate.m`. 3. 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? ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Braze Swift SDK url: https://github.com/braze-inc/braze-swift-sdk/ about: Consider upgrading to the new Braze Swift SDK for a better experience. - 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. ================================================ FILE: .github/ISSUE_TEMPLATE/feature.yml ================================================ 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 ================================================ FILE: .gitignore ================================================ .DS_Store Example/build/ Example/builds/ Pods/ Podfile.lock !Samples/**/Podfile.lock xcuserdata/ .idea/ **xcshareddata** # Emacs temporary files *~ # XCFrameworks *.xcframework ================================================ FILE: Appboy-Push-Story.podspec ================================================ Pod::Spec.new do |s| s.name = "Appboy-Push-Story" s.version = "4.7.0" s.summary = "This is the Braze Push Story SDK for Mobile Marketing Automation" s.homepage = "http://www.braze.com" s.license = { :type => 'Commercial', :text => 'Please refer to https://github.com/Appboy/appboy-ios-sdk/blob/master/LICENSE'} s.author = { "Appboy" => "http://www.braze.com" } s.source = { :http => "https://github.com/Appboy/appboy-ios-sdk/releases/download/#{s.version.to_s}/AppboyPushStory.zip" } s.platform = :ios s.ios.deployment_target = '11.0' s.requires_arc = true s.documentation_url = 'https://www.braze.com/docs' s.vendored_frameworks = 'AppboyPushStory/AppboyPushStory.xcframework' s.resource_bundle = { 'AppboyPushStory' => 'AppboyPushStory/Resources/*' } s.user_target_xcconfig = { 'OTHER_LDFLAGS' => '-ObjC' } end ================================================ FILE: Appboy-iOS-SDK.podspec ================================================ Pod::Spec.new do |s| s.name = "Appboy-iOS-SDK" s.version = "4.7.0" s.summary = "This is the Braze iOS SDK for Mobile Marketing Automation" s.homepage = "http://www.braze.com" s.license = { :type => 'Commercial', :text => 'Please refer to https://github.com/Appboy/appboy-ios-sdk/blob/master/LICENSE'} s.author = { "Appboy" => "http://www.braze.com" } s.source = { :http => "https://github.com/Appboy/appboy-ios-sdk/releases/download/#{s.version.to_s}/Appboy_iOS_SDK.zip" } s.platform = :ios s.ios.deployment_target = '11.0' s.requires_arc = true s.documentation_url = 'https://www.braze.com/docs' s.exclude_files = 'AppboyKit/**/*.txt' s.preserve_paths = 'AppboyKit/**/*.*' s.default_subspec = 'UI' s.pod_target_xcconfig = { 'OTHER_LDFLAGS' => '-ObjC' } s.subspec 'Core' do |sc| sc.ios.library = 'z' sc.frameworks = 'SystemConfiguration', 'QuartzCore', 'CoreText', 'WebKit' sc.source_files = 'AppboyKit/include/*.h', 'AppboyKit/ABKModalWebViewController.m', 'AppboyKit/ABKNoConnectionLocalization.m', 'AppboyKit/ABKLocationManagerProvider.m' sc.resource_bundle = { 'Appboy' => ['AppboyKit/Appboy.bundle/*.{lproj,txt,xcprivacy}'] } sc.vendored_framework = 'AppboyKit/AppboyKitLibrary.xcframework' sc.weak_framework = 'CoreTelephony', 'UserNotifications' end s.subspec 'UI' do |sui| sui.dependency 'Appboy-iOS-SDK/NewsFeed' sui.dependency 'Appboy-iOS-SDK/InAppMessage' sui.dependency 'Appboy-iOS-SDK/ContentCards' sui.dependency 'Appboy-iOS-SDK/Core' end s.subspec 'NewsFeed' do |snf| snf.source_files = 'AppboyUI/ABKNewsFeed/*.*', 'AppboyUI/ABKNewsFeed/ViewControllers/**/*.*', 'AppboyUI/ABKUIUtils/**/*.*', 'AppboyKit/ABKSDWebImageProxy.m' snf.resource_bundle = { 'AppboyUI.NewsFeed' => 'AppboyUI/ABKNewsFeed/Resources/**/*.*' } snf.dependency 'Appboy-iOS-SDK/Core' snf.dependency 'SDWebImage', '>= 5.18.7', '< 6' end s.subspec 'InAppMessage' do |siam| siam.source_files = 'AppboyUI/ABKUIUtils/**/*.*', 'AppboyUI/ABKInAppMessage/*.*', 'AppboyUI/ABKInAppMessage/ViewControllers/*.*', 'AppboyKit/ABKSDWebImageProxy.m' siam.resource_bundle = { 'AppboyUI.InAppMessage' => 'AppboyUI/ABKInAppMessage/Resources/*.*' } siam.dependency 'Appboy-iOS-SDK/Core' siam.dependency 'SDWebImage', '>= 5.18.7', '< 6' end s.subspec 'ContentCards' do |scc| scc.source_files = 'AppboyUI/ABKContentCards/*.*', 'AppboyUI/ABKContentCards/ViewControllers/**/*.*', 'AppboyUI/ABKUIUtils/**/*.*', 'AppboyKit/ABKSDWebImageProxy.m' scc.resource_bundle = { 'AppboyUI.ContentCards' => 'AppboyUI/ABKContentCards/Resources/**/*.*' } scc.dependency 'Appboy-iOS-SDK/Core' scc.dependency 'SDWebImage', '>= 5.18.7', '< 6' end end ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Headers/ABKAttributionData.h ================================================ #import /* * Braze Public API: ABKAttributionData */ NS_ASSUME_NONNULL_BEGIN @interface ABKAttributionData : NSObject /*! * @param network The attribution network * @param campaign The attribution campaign * @param adGroup The attribution adGroup * @param creative The attribution creative * * @discussion: Creates an ABKAttributionData object to send to Braze servers. */ - (instancetype)initWithNetwork:(nullable NSString *)network campaign:(nullable NSString *)campaign adGroup:(nullable NSString *)adGroup creative:(nullable NSString *)creative; @property (nonatomic, readonly, nullable) NSString *network; @property (nonatomic, readonly, nullable) NSString *campaign; @property (nonatomic, readonly, nullable) NSString *adGroup; @property (nonatomic, readonly, nullable) NSString *creative; @end NS_ASSUME_NONNULL_END ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Headers/ABKBannerCard.h ================================================ #import "ABKCard.h" /* * Braze Public API: ABKBannerCard */ NS_ASSUME_NONNULL_BEGIN @interface ABKBannerCard : ABKCard /* * This property is the URL of the card's image. */ @property (copy) NSString *image; /* * The link text for the property url, like @"blog.appboy.com". It can be displayed on the card's * UI to indicate the action/direction of clicking on the card. */ @property (copy, nullable) NSString *domain; /* * This property is the aspect ratio of the card's image. It is meant to serve as a hint before * image loading completes. Note that the property may not be supplied in certain circumstances. */ @property float imageAspectRatio; @end NS_ASSUME_NONNULL_END ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Headers/ABKBannerContentCard.h ================================================ #import "ABKContentCard.h" @interface ABKBannerContentCard : ABKContentCard /* * The URL of the card's image. */ @property (copy) NSString *image; /* * This property is the aspect ratio of the card's image. It is meant to serve as a hint before * image loading completes. Note that the property may not be supplied in certain circumstances. */ @property float imageAspectRatio; @end ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Headers/ABKCaptionedImageCard.h ================================================ #import "ABKCard.h" /* * Braze Public API: ABKCaptionedImageCard */ NS_ASSUME_NONNULL_BEGIN @interface ABKCaptionedImageCard : ABKCard /* * This property is the URL of the card's image. */ @property (copy) NSString *image; /* * This property is the aspect ratio of the card's image. It is meant to serve as a hint before * image loading completes. Note that the property may not be supplied in certain circumstances. */ @property float imageAspectRatio; /* * The title text for the card. */ @property (copy) NSString *title; /* * The description text for the card. */ @property (copy) NSString *cardDescription; /* * The link text for the property url, like @"blog.appboy.com". It can be displayed on the card's * UI to indicate the action/direction of clicking on the card. */ @property (copy, nullable) NSString *domain; @end NS_ASSUME_NONNULL_END ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Headers/ABKCaptionedImageContentCard.h ================================================ #import "ABKContentCard.h" NS_ASSUME_NONNULL_BEGIN @interface ABKCaptionedImageContentCard : ABKContentCard /* * The URL of the card's image. */ @property (copy) NSString *image; /* * This property is the aspect ratio of the card's image. It is meant to serve as a hint before * image loading completes. Note that the property may not be supplied in certain circumstances. */ @property float imageAspectRatio; /* * The title text for the card. */ @property (copy) NSString *title; /* * The description text for the card. */ @property (copy) NSString *cardDescription; /* * The link text for the property url, like @"blog.appboy.com". It can be displayed on the card's * UI to indicate the action/direction of clicking on the card. */ @property (copy, nullable) NSString *domain; @end NS_ASSUME_NONNULL_END ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Headers/ABKCard.h ================================================ #import #import "ABKFeedController.h" /* * Braze Public API: ABKCard */ NS_ASSUME_NONNULL_BEGIN @interface ABKCard : NSObject /* * Card's ID. */ @property (readonly) NSString *idString; /* * This property reflects if the card is read or unread by the user. */ @property (nonatomic) BOOL viewed; /* * The property is the unix timestamp of the card's creation time from Braze dashboard. */ @property (nonatomic, readonly) double created; /* * The property is the unix timestamp of the card's latest update time from Braze dashboard. */ @property (nonatomic, readonly) double updated; /* * The categories assigned to the card. */ @property ABKCardCategory categories; /* * The property is the unix timestamp of the card's expiration time. When the value is less than 0, it means the card * doesn't an expire date. */ @property (readonly) double expiresAt; /*! * This property carries extra data in the form of an NSDictionary which can be sent down via the Braze Dashboard. * You may want to design and implement a custom handler to access this data depending on your use case. */ @property (strong, nullable) NSDictionary *extras; //Optional: /* * The URL string that will be opened after the card is clicked on. */ @property (copy, nullable) NSString *urlString; /*! * When the card's urlString is not nil, if the property is set to YES, the URL will be opened in a modal WKWebView * inside the app. If this property is set to NO, the URL will be opened by the OS and web URLs will be opened in * an external web browser app. * * This property defaults to NO. */ @property BOOL openUrlInWebView; /* * @param cardDictionary The dictionary for card deserialization. * * Deserializes the dictionary to a card for use by wrappers such as Braze's Unity SDK for iOS. * When the deserialization isn't successful, this method returns nil; otherwise, it returns the deserialized card. */ + (nullable ABKCard *)deserializeCardFromDictionary:(nullable NSDictionary *)cardDictionary; /* * Serializes the card to binary data for use by wrappers such as Braze's Unity SDK for iOS. */ - (nullable NSData *)serializeToData; /* * Manually log an impression to Braze for the card. * This should only be used for custom news feed view controller. ABKFeedViewController already has card impression logging. */ - (void)logCardImpression; /* * Manually log a click to Braze for the card. * This should only be used for custom news feed view controller. ABKFeedViewController already has card click logging. * The SDK will only log a card click when the card has the url property with a valid url. */ - (void)logCardClicked; - (BOOL)hasSameId:(ABKCard *)card; @end NS_ASSUME_NONNULL_END ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Headers/ABKClassicCard.h ================================================ #import "ABKCard.h" /* * Braze Public API: ABKClassicCard */ NS_ASSUME_NONNULL_BEGIN @interface ABKClassicCard : ABKCard /* * This property is the URL of the card's image. */ @property (copy, nullable) NSString *image; /* * The description text for the card. */ @property (copy) NSString *cardDescription; /* * The news title text for the card. */ @property (copy, nullable) NSString *title; /* * The link text for the property url, like @"blog.appboy.com". It can be displayed on the card's * UI to indicate the action/direction of clicking on the card. */ @property (copy, nullable) NSString *domain; @end NS_ASSUME_NONNULL_END ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Headers/ABKClassicContentCard.h ================================================ #import "ABKContentCard.h" NS_ASSUME_NONNULL_BEGIN @interface ABKClassicContentCard : ABKContentCard /* * The URL of the card's image. */ @property (copy, nullable) NSString *image; /* * The news title text for the card. */ @property (copy) NSString *title; /* * The description text for the card. */ @property (copy) NSString *cardDescription; /* * The link text for the property url, like @"blog.appboy.com". It can be displayed on the card's * UI to indicate the action/direction of clicking on the card. */ @property (copy, nullable) NSString *domain; @end NS_ASSUME_NONNULL_END ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Headers/ABKContentCard.h ================================================ #import /* * Braze Public API: ABKContentCard */ NS_ASSUME_NONNULL_BEGIN @interface ABKContentCard : NSObject /*! * Card's ID. */ @property (readonly) NSString *idString; /*! * This property reflects if the card is read or unread by the user. */ @property (nonatomic) BOOL viewed; /*! * The property is the unix timestamp of the card's creation time from Braze dashboard. */ @property (nonatomic, readonly) double created; /*! * The property is the unix timestamp of the card's expiration time. When the value is less than 0, it means the card * doesn't an expire date. */ @property (readonly) double expiresAt; /*! * This property reflects if the card can be dismissed by the user. */ @property (nonatomic) BOOL dismissible; /*! * This property reflects if the card has been pinned by the user. */ @property (nonatomic) BOOL pinned; /*! * This property reflects if the card has been dimissed. */ @property (nonatomic) BOOL dismissed; /*! * This property reflects if the card has been clicked. */ @property (nonatomic) BOOL clicked; /*! * This property carries extra data in the form of an NSDictionary which can be sent down via the Braze Dashboard. * You may want to design and implement a custom handler to access this data depending on your use case. */ @property (strong, nullable) NSDictionary *extras; /*! * This property is set to YES if the instance represents a test content card */ @property (nonatomic, readonly) BOOL isTest; /*! * The URL string that will be opened after the card is clicked on. */ @property (copy, nullable) NSString *urlString; /*! * When the card's urlString is not nil, if the property is set to YES, the URL will be opened in a modal WKWebView * inside the app. If this property is set to NO, the URL will be opened by the OS and web URLs will be opened in * an external web browser app. * * This property defaults to NO. */ @property BOOL openUrlInWebView; /*! * @param cardDictionary The dictionary for card deserialization. * * Deserializes the dictionary to a card for use by wrappers such as Braze's Unity SDK for iOS. * When the deserialization isn't successful, this method returns nil; otherwise, it returns the deserialized card. */ + (nullable ABKContentCard *)deserializeCardFromDictionary:(nullable NSDictionary *)cardDictionary; /*! * Serializes the card to binary data for use by wrappers such as Braze's Unity SDK for iOS. */ - (nullable NSData *)serializeToData; /*! * Manually log an impression to Braze for the card. * This should only be used for custom content card view controllers. */ - (void)logContentCardImpression; /*! * Manually log a click to Braze for the card. * This should only be used for custom contentcard view controllers. */ - (void)logContentCardClicked; /*! * Manually dismiss a card. * Sets the card's `dismissed` property to YES and logs the dismissal to Braze. * Only has effect if the card is dismissible and if the `dismissed` property is currently set to NO. */ - (void)logContentCardDismissed; - (BOOL)isControlCard; - (BOOL)hasSameId:(ABKContentCard *)card; @end NS_ASSUME_NONNULL_END ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Headers/ABKFacebookUser.h ================================================ #import #import "ABKUser.h" NS_ASSUME_NONNULL_BEGIN extern NSInteger const DefaultNumberOfFriends; /* * Braze Public API: ABKFacebookUser */ @interface ABKFacebookUser : NSObject /*! * @param facebookUserDictionary The dictionary returned from facebook with facebook graph api endpoint "/me". Please * refer to https://developers.facebook.com/docs/graph-api/reference/v4.0/user for more information. * @param numberOfFriends The length of the friends array from facebook. You can get the array from the dictionary returned * from facebook with facebook graph api endpoint "/me/friends", under the key "data". Please refer to * https://developers.facebook.com/docs/graph-api/reference/v4.0/user/friends for more information. * @param likes The array of user's facebook likes from facebook. You can get the array from the dictionary returned * from facebook with facebook graph api endpoint "/me/likes", under the key "data"; Please refer to * https://developers.facebook.com/docs/graph-api/reference/v4.0/user/likes for more information. * * @discussion: This method is to generate a ABKFacebookUser so you can pass the user's facebook account data to Braze. * After a ABKFacebookUser object is generated, you can check the value of properties but you cannot change it. * If you want to update the user's facebook data, you need to generate a new ABKFacebookUser instance and set it as * [Appboy sharedInstance].user.facebookUser. */ - (instancetype)initWithFacebookUserDictionary:(nullable NSDictionary *)facebookUserDictionary numberOfFriends:(NSInteger)numberOfFriends likes:(nullable NSArray *)likes; @property (readonly, nullable) NSDictionary *facebookUserDictionary; @property (readonly) NSInteger numberOfFriends; @property (readonly, nullable) NSArray *likes; @end NS_ASSUME_NONNULL_END ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Headers/ABKFeedController.h ================================================ #import /* ------------------------------------------------------------------------------------------------------ * Notifications */ /*! * When the news feed is updated, Braze will post a notification through the NSNotificationCenter. * The name of the notification is the string constant referred to by ABKFeedUpdatedNotification. The * userInfo dictionary associated with the notification will has one object, with key the same string * as ABKFeedUpdatedIsSuccessfulKey, to indicate whether the update is successful or not. * * To listen for this notification, you would register an object as an observer of the notification * using something like: * *
 *   [[NSNotificationCenter defaultCenter] addObserver:self
 *                                            selector:@selector(feedUpdatedNotificationReceived:)
 *                                                name:ABKFeedUpdatedNotification
 *                                              object:nil];
 * 
* * where "feedUpdatedNotificationReceived:" is your callback method for handling the notification: * *
 *   - (void)feedUpdatedNotificationReceived:(NSNotification *)notification {
 *     BOOL updateIsSuccessful = [notification.userInfo[ABKFeedUpdatedIsSuccessfulKey] boolValue];
 *     < Do something in response to the notification >
 *   }
 * 
*/ NS_ASSUME_NONNULL_BEGIN extern NSString *const ABKFeedUpdatedNotification; extern NSString *const ABKFeedUpdatedIsSuccessfulKey; /* ------------------------------------------------------------------------------------------------------ * Enums */ /*! * Values representing the news feed cards' categories recognized by the SDK. */ typedef NS_OPTIONS(NSUInteger, ABKCardCategory) { ABKCardCategoryNoCategory = 1 << 0, ABKCardCategoryNews = 1 << 1, ABKCardCategoryAdvertising = 1 << 2, ABKCardCategoryAnnouncements = 1 << 3, ABKCardCategorySocial = 1 << 4, ABKCardCategoryAll = 1 << 0 | 1 << 1 | 1 << 2 | 1 << 3 | 1 << 4 }; /* * Braze Public API: ABKFeedController */ @interface ABKFeedController : NSObject /*! * The latest cards of the Braze News Feed saved in memory and disk. Right now the available card types are ABKBannerCard, * ABKCaptionedImageCard, ABKClassicCard and ABKTextAnnouncementCard. They are all subclasses * of ABKCard. */ @property (readonly, getter=getNewsFeedCards) NSArray *newsFeedCards; /*! * The NSDate object that indicates the last time the newsFeedCards property was updated from the Braze server. */ @property (readonly, nullable) NSDate *lastUpdate; /*! * This method returns the number of currently active cards which have not been viewed in the given categories. * A "view" happens when a card becomes visible in the feed view. This differentiates * between cards which are off-screen in the scrolling view, and those which * are on-screen; when a card scrolls onto the screen, it's counted as viewed. * * Cards are counted as viewed only once -- if a card scrolls off the screen and * back on, it's not re-counted. * * Cards are counted only once even if they appear in multiple feed views or across multiple devices. */ - (NSInteger)unreadCardCountForCategories:(ABKCardCategory)categories; /*! * This method returns the total number of currently active cards belongs to given categories. Cards are * counted only once even if they appear in multiple feed views. */ - (NSInteger)cardCountForCategories:(ABKCardCategory)categories; /*! * @param categories An ABKCardCategory indicating the categories that you want to get. You can pass more than one category * at one time by using "|" to separate categories like: ABKCardCategoryNews | ABKCardCategoryAnnouncements | ABKCardCategorySocial * @return An array of cards of the given categories. * * @discussion This method will find the cards of given categories and return them. * When the given categories don't exist in any card, this method will return an empty array. */ - (NSArray *)getCardsInCategories:(ABKCardCategory)categories; @end NS_ASSUME_NONNULL_END ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Headers/ABKSdkAuthenticationDelegate.h ================================================ #import #import "ABKSdkAuthenticationError.h" /* * Braze Public API: ABKSdkAuthenticationDelegate */ NS_ASSUME_NONNULL_BEGIN @protocol ABKSdkAuthenticationDelegate /*! * This method is fired when an SDK Authentication error is returned by the server, for example, if * the signature is expired or invalid. * * You are responsible for providing the Braze SDK a valid signature when this delegate method is * called. * SDK requests will retry periodically using an exponential backoff approach. After 50 consecutive * failed attempts, retries will be paused until the next session start. * * @param authError The SDK Authentication error returned by the server */ - (void)handleSdkAuthenticationError:(ABKSdkAuthenticationError *)authError; @end NS_ASSUME_NONNULL_END ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Headers/ABKSdkAuthenticationError.h ================================================ #import /* * Braze Public API: ABKSdkAuthenticationError */ NS_ASSUME_NONNULL_BEGIN @interface ABKSdkAuthenticationError : NSObject /*! * The error code for the SDK Authentication failure. */ @property (readonly) NSInteger code; /*! * The reason for the SDK Authentication failure. */ @property (nullable, readonly) NSString *reason; /*! * The user ID associated with the request that failed due to SDK Authentication failure. */ @property (nullable, readonly) NSString *userId; /*! * The signature that was sent with the request that failed due to SDK Authentication failure. */ @property (readonly) NSString *signature; - (instancetype)initWithCode:(NSInteger)code reason:(NSString *)reason userId:(NSString *)userId signature:(NSString *)signature; @end NS_ASSUME_NONNULL_END ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Headers/ABKSdkMetadata.h ================================================ /*! * Enum representing the accepted SDK Metatadata. * See addSdkMetadata for more details. */ typedef NSString *ABKSdkMetadata NS_TYPED_EXTENSIBLE_ENUM; extern ABKSdkMetadata const ABKSdkMetadataAdjust; extern ABKSdkMetadata const ABKSdkMetadataAirBridge; extern ABKSdkMetadata const ABKSdkMetadataAppsFlyer; extern ABKSdkMetadata const ABKSdkMetadataBluedot; extern ABKSdkMetadata const ABKSdkMetadataBranch; extern ABKSdkMetadata const ABKSdkMetadataCordova; extern ABKSdkMetadata const ABKSdkMetadataCarthage; extern ABKSdkMetadata const ABKSdkMetadataCocoaPods; extern ABKSdkMetadata const ABKSdkMetadataCordovaPM; extern ABKSdkMetadata const ABKSdkMetadataExpo; extern ABKSdkMetadata const ABKSdkMetadataFoursquare; extern ABKSdkMetadata const ABKSdkMetadataFlutter; extern ABKSdkMetadata const ABKSdkMetadataGoogleTagManager; extern ABKSdkMetadata const ABKSdkMetadataGimbal; extern ABKSdkMetadata const ABKSdkMetadataGraddle; extern ABKSdkMetadata const ABKSdkMetadataIonic; extern ABKSdkMetadata const ABKSdkMetadataKochava; extern ABKSdkMetadata const ABKSdkMetadataManual; extern ABKSdkMetadata const ABKSdkMetadataMParticle; extern ABKSdkMetadata const ABKSdkMetadataNativeScript; extern ABKSdkMetadata const ABKSdkMetadataNPM; extern ABKSdkMetadata const ABKSdkMetadataNuGet; extern ABKSdkMetadata const ABKSdkMetadataPub; extern ABKSdkMetadata const ABKSdkMetadataRadar; extern ABKSdkMetadata const ABKSdkMetadataReactNative; extern ABKSdkMetadata const ABKSdkMetadataSegment; extern ABKSdkMetadata const ABKSdkMetadataSingular; extern ABKSdkMetadata const ABKSdkMetadataSwiftPM; extern ABKSdkMetadata const ABKSdkMetadataTealium; extern ABKSdkMetadata const ABKSdkMetadataUnreal; extern ABKSdkMetadata const ABKSdkMetadataUnityPM; extern ABKSdkMetadata const ABKSdkMetadataUnity; extern ABKSdkMetadata const ABKSdkMetadataVizbee; extern ABKSdkMetadata const ABKSdkMetadataXamarin; ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Headers/ABKTextAnnouncementCard.h ================================================ #import "ABKCard.h" /* * Braze Public API: ABKTextAnnouncementCard */ NS_ASSUME_NONNULL_BEGIN @interface ABKTextAnnouncementCard : ABKCard /* * The title text for the card. */ @property (copy) NSString *title; /* * The description text for the card. */ @property (copy) NSString *cardDescription; /* * The link text for the property url, like @"blog.appboy.com". It can be displayed on the card's * UI to indicate the action/direction of clicking on the card. */ @property (copy, nullable) NSString *domain; @end NS_ASSUME_NONNULL_END ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Headers/ABKTwitterUser.h ================================================ #import /* * Braze Public API: ABKTwitterUser */ NS_ASSUME_NONNULL_BEGIN @interface ABKTwitterUser : NSObject /*! * The value returned from Twitter's Users API with key "description". Please refer to * https://dev.twitter.com/overview/api/users for more information. */ @property (copy, nullable) NSString* userDescription; /*! * The value returned from Twitter's Users API with key "name". Please refer to * https://dev.twitter.com/overview/api/users for more information. */ @property (copy, nullable) NSString* twitterName; /*! * The value returned from Twitter's Users API with key "profile_image_url". Please refer to * https://dev.twitter.com/overview/api/users for more information. */ @property (copy, nullable) NSString* profileImageUrl; /*! * The value returned from Twitter's Users API with key "screen_name". Please refer to * https://dev.twitter.com/overview/api/users for more information. */ @property (copy, nullable) NSString* screenName; /*! * The value returned from Twitter's Users API with key "followers_count". Please refer to * https://dev.twitter.com/overview/api/users for more information. */ @property NSInteger followersCount; /*! * The value returned from Twitter's Users API with key "friends_count". Please refer to * https://dev.twitter.com/overview/api/users for more information. */ @property NSInteger friendsCount; /*! * The value returned from Twitter's Users API with key "statuses_count". Please refer to * https://dev.twitter.com/overview/api/users for more information. */ @property NSInteger statusesCount; /*! * The value returned from Twitter's Users API with key "id". Please refer to * https://dev.twitter.com/overview/api/users for more information. */ @property NSInteger twitterID; @end NS_ASSUME_NONNULL_END ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Headers/ABKUser.h ================================================ // // ABKUser.h // AppboySDK #import @class ABKFacebookUser; @class ABKTwitterUser; @class ABKAttributionData; NS_ASSUME_NONNULL_BEGIN /* ------------------------------------------------------------------------------------------------------ * Enums */ /*! * Genders recognized by the SDK. */ typedef NS_ENUM(NSInteger, ABKUserGenderType) { ABKUserGenderMale, ABKUserGenderFemale, ABKUserGenderOther, ABKUserGenderUnknown, ABKUserGenderNotApplicable, ABKUserGenderPreferNotToSay }; /*! * Convenience enum to represent notification status, for email and push notifications. * * OPTED_IN: subscribed, and explicitly opted in. * SUBSCRIBED: subscribed, but not explicitly opted in. * UNSUBSCRIBED: unsubscribed and/or explicitly opted out. */ typedef NS_ENUM(NSInteger, ABKNotificationSubscriptionType) { ABKOptedIn, ABKSubscribed, ABKUnsubscribed }; /*! * When setting the custom attributes with custom keys: * 1. The maximum key length is 255 characters; longer keys are truncated. * 2. The maximum length for a string value in a custom attribute is 255 characters; longer values are truncated. */ /* * Braze Public API: ABKUser */ @interface ABKUser : NSObject /*! * The User's first name (String) */ @property (nonatomic, copy, nullable) NSString *firstName; /*! * The User's last name (String) */ @property (nonatomic, copy, nullable) NSString *lastName; /*! * The User's email (String) */ @property (nonatomic, copy, nullable) NSString *email; /*! * The User's date of birth (NSDate) */ @property (nonatomic, copy, nullable) NSDate *dateOfBirth; /*! * The User's country (String) */ @property (nonatomic, copy, nullable) NSString *country; /*! * The User's home city (String) */ @property (nonatomic, copy, nullable) NSString *homeCity; /*! * The User's language (String) * * Language Strings should be valid ISO 639-1 language codes. * See https://www.loc.gov/standards/iso639-2/php/code_list.php. * * If not set here, user language will be inferred from the device language. */ @property (nonatomic, copy, nullable) NSString *language; /*! * The User's phone number (String) */ @property (nonatomic, copy, nullable) NSString *phone; @property (nonatomic, copy, nullable, readonly) NSString *userID; /*! * The User's avatar image URL. This URL will be processed by the server and used in their user profile on the * dashboard. (String) */ @property (nonatomic, copy, nullable) NSString *avatarImageURL; /*! * The User's Facebook account information. For more detail, please refer to ABKFacebookUser.h. */ @property (strong, nullable) ABKFacebookUser *facebookUser; /*! * The User's Twitter account information. For more detail, please refer to ABKTwitterUser.h. */ @property (strong, nullable) ABKTwitterUser *twitterUser; /*! * Sets the attribution information for the user. For in apps that have an install tracking integration. * For more information, please refer to ABKAttributionData.h. */ @property (strong, nullable) ABKAttributionData *attributionData; /*! * Adds an an alias for the current user. Individual (alias, label) pairs can exist on one and only one user. * If a different user already has this alias or external user id, the alias attempt will be rejected * on the server. * * @param alias The alias of the current user. * @param label The label of the alias; used to differentiate it from other aliases for the user. * @return Whether or not the alias and label are valid. Does not guarantee they won't collide with * an existing pair. */ - (BOOL)addAlias:(NSString *)alias withLabel:(NSString *)label; /*! * @param gender ABKUserGender enum representing the user's gender. * @return YES if the user gender is set properly */ - (BOOL)setGender:(ABKUserGenderType)gender; /*! * Sets whether or not the user should be sent email campaigns. Setting it to unsubscribed opts the user out of * an email campaign that you create through the Braze dashboard. * * @param emailNotificationSubscriptionType enum representing the user's email notifications subscription type. * @return YES if the field is set successfully, else NO. */ - (BOOL)setEmailNotificationSubscriptionType:(ABKNotificationSubscriptionType)emailNotificationSubscriptionType; /*! * Sets the push notification subscription status of the user. Used to collect information about the user. * * @param pushNotificationSubscriptionType enum representing the user's push notifications subscription type. * @return YES if the field is set successfully, else NO. */ - (BOOL)setPushNotificationSubscriptionType:(ABKNotificationSubscriptionType)pushNotificationSubscriptionType; /*! * Adds the user to a Subscription Group. * * @param groupId The string UUID corresponding to the subscription group, provided by the Braze dashboard. * @return YES if the user was successfully added, else NO. If not, the groupId might have been nil or invalid. */ - (BOOL)addToSubscriptionGroupWithGroupId:(NSString *)groupId; /*! * Removes the user from a Subscription Group. * * @param groupId The string UUID corresponding to the subscription group, provided by the Braze dashboard. * @return YES if the user was successfully removed, else NO. If not, the groupId might have been nil or invalid. */ - (BOOL)removeFromSubscriptionGroupWithGroupId:(NSString *)groupId; /*! * @param key The String name of the custom user attribute * @param value A boolean value to set as a custom attribute * @return whether or not the custom user attribute was set successfully; If not, your key might have been nil or empty, * your value might have been invalid (either nil, or not of the correct type), or you tried to set a value for * one of the reserved keys. Please check the log for more details about the specific failure you encountered. */ - (BOOL)setCustomAttributeWithKey:(NSString *)key andBOOLValue:(BOOL)value; /*! * @param key The String name of the custom user attribute * @param value An integer value to set as a custom attribute * @return whether or not the custom user attribute was set successfully; If not, your key might have been nil or empty, * your value might have been invalid (either nil, or not of the correct type), or you tried to set a value for * one of the reserved keys. Please check the log for more details about the specific failure you encountered. */ - (BOOL)setCustomAttributeWithKey:(NSString *)key andIntegerValue:(NSInteger)value; /*! * @param key The String name of the custom user attribute * @param value A double value to set as a custom attribute * @return whether or not the custom user attribute was set successfully; If not, your key might have been nil or empty, * your value might have been invalid (either nil, or not of the correct type), or you tried to set a value for * one of the reserved keys. Please check the log for more details about the specific failure you encountered. */ - (BOOL)setCustomAttributeWithKey:(NSString *)key andDoubleValue:(double)value; /*! * @param key The String name of the custom user attribute * @param value An NSString value to set as a custom attribute * @return whether or not the custom user attribute was set successfully; If not, your key might have been nil or empty, * your value might have been invalid (either nil, or not of the correct type), or you tried to set a value for * one of the reserved keys. Please check the log for more details about the specific failure you encountered. */ - (BOOL)setCustomAttributeWithKey:(NSString *)key andStringValue:(NSString *)value; /*! * @param key The String name of the custom user attribute * @param value An NSDate value to set as a custom attribute * @return whether or not the custom user attribute was set successfully; If not, your key might have been nil or empty, * your value might have been invalid (either nil, or not of the correct type), or you tried to set a value for * one of the reserved keys. Please check the log for more details about the specific failure you encountered. */ - (BOOL)setCustomAttributeWithKey:(NSString *)key andDateValue:(NSDate *)value; /*! * @param key The String name of the custom user attribute to unset * @return whether or not the custom user attribute was unset successfully */ - (BOOL)unsetCustomAttributeWithKey:(NSString *)key; /** * Increments the value of an custom attribute by one. Only integer and long custom attributes can be incremented. * Attempting to increment a custom attribute that is not an integer or a long will be ignored. If you increment a * custom attribute that has not previously been set, a custom attribute will be created and assigned a value of one. * * @param key The identifier of the custom attribute * @return YES if the increment for the custom attribute of given key is saved */ - (BOOL)incrementCustomUserAttribute:(NSString *)key; /** * Increments the value of an custom attribute by a given amount. Only integer and long custom attributes can be * incremented. Attempting to increment a custom attribute that is not an integer or a long will be ignored. If * you increment a custom attribute that has not previously been set, a custom attribute will be created and assigned * the value of incrementValue. To decrement the value of a custom attribute, use a negative incrementValue. * * @param key The identifier of the custom attribute * @param incrementValue The amount by which to increment the custom attribute * @return YES if the increment for the custom attribute of given key is saved */ - (BOOL)incrementCustomUserAttribute:(NSString *)key by:(NSInteger)incrementValue; /** * Adds the string value to a custom attribute string array specified by the key. If you add a key that has not * previously been set, a custom attribute string array will be created containing the value. * * @param key The custom attribute key * @param value A string to be added to the custom attribute string array * @return YES if the operation was successful */ - (BOOL)addToCustomAttributeArrayWithKey:(NSString *)key value:(NSString *)value; /** * Removes the string value from a custom attribute string array specified by the key. If you remove a key that has not * previously been set, nothing will be changed. * * @param key The custom attribute key * @param value A string to be removed from the custom attribute string array * @return YES if the operation was successful */ - (BOOL)removeFromCustomAttributeArrayWithKey:(NSString *)key value:(NSString *)value; /** * Sets a string array from a custom attribute specified by the key. * * @param key The custom attribute key * @param valueArray A string array to set as a custom attribute. If this value is nil, then Braze will unset the custom * attribute and remove the corresponding array if there is one. * @return YES if the operation was successful */ - (BOOL)setCustomAttributeArrayWithKey:(NSString *)key array:(nullable NSArray *)valueArray; /*! * Sets the last known location for the user. Intended for use with ABKDisableLocationAutomaticTrackingOptionKey set to YES * when starting Braze, so that the only locations being set are by the integrating app. Otherwise, calls to this * method will be contending with automatic location update events. * * @param latitude The latitude of the User's location in degrees, the number should be in the range of [-90, 90] * @param longitude The longitude of the User's location in degrees, the number should be in the range of [-180, 180] * @param horizontalAccuracy The accuracy of the User's horizontal location in meters, the number should not be negative */ - (BOOL)setLastKnownLocationWithLatitude:(double)latitude longitude:(double)longitude horizontalAccuracy:(double)horizontalAccuracy; /*! * Sets the last known location for the user. Intended for use with ABKDisableLocationAutomaticTrackingOptionKey set to YES * when starting Braze, so that the only locations being set are by the integrating app. Otherwise, calls to this * method will be contending with automatic location update events. * * @param latitude The latitude of the User's location in degrees, the number should be in the range of [-90, 90] * @param longitude The longitude of the User's location in degrees, the number should be in the range of [-180, 180] * @param horizontalAccuracy The accuracy of the User's horizontal location in meters, the number should not be negative * @param altitude The altitude of the User's location in meters * @param verticalAccuracy The accuracy of the User's vertical location in meters, the number should not be negative */ - (BOOL)setLastKnownLocationWithLatitude:(double)latitude longitude:(double)longitude horizontalAccuracy:(double)horizontalAccuracy altitude:(double)altitude verticalAccuracy:(double)verticalAccuracy; /*! * Adds the location custom attribute for the user. * * @param key The custom attribute key * @param latitude The latitude of the location in degrees, the number should be in the range of [-90, 90] * @param longitude The longitude of the location in degrees, the number should be in the range of [-180, 180] */ - (BOOL)addLocationCustomAttributeWithKey:(NSString *)key latitude:(double)latitude longitude:(double)longitude; /*! * Removes the location custom attribute for the user. * * @param key The custom attribute key */ - (BOOL)removeLocationCustomAttributeWithKey:(NSString *)key; @end NS_ASSUME_NONNULL_END ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Headers/Appboy.h ================================================ // // Appboy.h // AppboySDK /*! \mainpage This site contains technical documentation for the %Braze iOS SDK. Click on the "Classes" link above to view the %Braze public interface classes and start integrating the SDK into your app! */ #import #import #import #import "ABKSdkMetadata.h" #ifndef APPBOY_SDK_VERSION #define APPBOY_SDK_VERSION @"4.7.0" #endif #if !TARGET_OS_TV @class ABKInAppMessageController; @class ABKInAppMessage; @class ABKInAppMessageViewController; #endif @class ABKUser; @class ABKFeedController; @class ABKContentCardsController; @class ABKLocationManager; @protocol ABKInAppMessageControllerDelegate; @protocol ABKIDFADelegate; @protocol ABKURLDelegate; @protocol ABKImageDelegate; @protocol ABKSdkAuthenticationDelegate; NS_ASSUME_NONNULL_BEGIN /* ------------------------------------------------------------------------------------------------------ * Keys for Braze startup options */ /*! * If you want to set the request policy at app startup time (useful for avoiding any automatic data requests made by * Braze at startup if you're looking to have full manual control). You can include one of the * ABKRequestProcessingPolicy enum values as the value for the ABKRequestProcessingPolicyOptionKey in the appboyOptions * dictionary. */ extern NSString *const ABKRequestProcessingPolicyOptionKey; /*! * Sets the data flush interval (in seconds). This only has an effect when the request processing mode is set to * ABKAutomaticRequestProcessing (which is the default). Values are converted into NSTimeIntervals and must be greater * than 1.0. */ extern NSString *const ABKFlushIntervalOptionKey; /*! * This key can be set to YES or NO and will configure whether Braze will automatically collect location (if the user permits). * If set to YES, Braze will collect location if authorized. * If it is set to NO or omitted, location will not be recorded for the user unless you manually * call setUserLastKnownLocation on ABKUser. */ extern NSString *const ABKEnableAutomaticLocationCollectionKey; /*! * This key can be set to YES or NO and will configure whether geofences are enabled. * If set to YES, geofences will be enabled. * If set to NO, geofences will be disabled. * If the field is omitted, we will use the value of ABKEnableAutomaticLocationCollectionKey. */ extern NSString *const ABKEnableGeofencesKey; /*! * This key can be set to YES or NO and will configure whether geofence requests are made automatically. * If set to YES, geofence requests will not be made automatically. * If set to NO, geofence requests will be made automatically. This is the default value when you have geofences enabled. */ extern NSString *const ABKDisableAutomaticGeofenceRequestsKey; /*! * This key can be set to an instance of a class that extends ABKIDFADelegate, which can be used to pass advertiser tracking information to to Braze. */ extern NSString *const ABKIDFADelegateKey; /*! * This key can be set to a custom API endpoint. This gets sent in the format sdk.api.braze.eu. */ extern NSString *const ABKEndpointKey; /*! * This key can be set to an instance of a class that conforms to the ABKURLDelegate protocol, allowing it to handle URLs in a custom way. */ extern NSString *const ABKURLDelegateKey; /*! * This can can be set to an instance of a class that conforms to the ABKImageDelegate protocol, allowing flexibility for using custom image libraries. */ extern NSString *const ABKImageDelegateKey; /*! * This key can be set to an instance of a class that conforms to the ABKInAppMessageControllerDelegate protocol, allowing it to handle in-app messages in a custom way. */ extern NSString *const ABKInAppMessageControllerDelegateKey; /*! * This key can be set YES or NO and will configure whether a modal in-app message will be dismissed when the user clicks * outside of the in-app message. * If set to YES, the in-app message will be dismissed. * If set to NO, the in-app message will not be dismissed. This is the default value. */ extern NSString *const ABKEnableDismissModalOnOutsideTapKey; /*! * This key can be set YES or NO and will configure whether the SDK Authentication feature is enabled. */ extern NSString *const ABKEnableSDKAuthenticationKey; /*! * This key can can be set to an instance of a class that conforms to the ABKSdkAuthenticationDelegate protocol, allowing it to handle * SDK Authentication errors. Setting this delegate will cause the delegate method `handleSdkAuthenticationError:` to get called in * the event of an SDK Authentication error. */ extern NSString *const ABKSdkAuthenticationDelegateKey; /*! * Set the time interval for session time out (in seconds). This will affect the case when user has a session shorter than * the set time interval. In that case, the session won't be close even though the user closed the app, but will continue until * it times out. The value should be an integer bigger than 0. */ extern NSString *const ABKSessionTimeoutKey; /*! * Set the minimum time interval in seconds between triggers. After a trigger happens, we will ignore any triggers until * the minimum time interval elapses. The default value is 30s. The minimum valid value is 0s. */ extern NSString *const ABKMinimumTriggerTimeIntervalKey; /*! * Key to report the SDK flavor currently being used. For internal use only. */ extern NSString *const ABKSDKFlavorKey; /*! * Key to specify an allowlist for device fields that are collected by the Braze SDK. * * To specify allowlisted device fields, assign the bitwise `OR` of desired fields to this key. Fields are defined * in `ABKDeviceOptions`. To turn off all fields, set the value of this key to `ABKDeviceOptionNone`. By default, * all fields are collected. */ extern NSString *const ABKDeviceAllowlistKey; /*! * This key is deprecated in favor of ABKDeviceAllowlistKey. See ABKDeviceAllowlistKey for more details. */ extern NSString *const ABKDeviceWhitelistKey __deprecated_msg("ABKDeviceWhitelistKey is deprecated. Please use ABKDeviceAllowlistKey instead."); extern NSString *const ABKEphemeralEventsKey; /*! * This key can be set to a string value representing the app group name for the Push Story Notification * Content extension. This is required for the SDK to fetch data from and handle user interactions * with the Push Story app extension. */ extern NSString *const ABKPushStoryAppGroupKey; /*! * This key can be set to an integer value to specify the level of the log statements output by the Braze SDK. * * The default log level is 8 and will minimally log info. To enable verbose logging for debugging, use log level 0. * * This selection will override any LogLevel value set in the Info.plist. */ extern NSString *const ABKLogLevelKey; /* ------------------------------------------------------------------------------------------------------ * Enums */ /*! * Possible values for the SDK's request processing policies: * ABKAutomaticRequestProcessing (default) - All server communication is handled automatically. This includes flushing * analytics data to the server, updating the feed, and requesting new in-app messages. Braze's * communication policy is to perform immediate server requests when user facing data is required (new in-app messages, * feed refreshes, etc.), and to otherwise perform periodic flushes of new analytics data every few seconds. * The interval between periodic flushes can be set explicitly using the ABKFlushInterval startup option. * ABKAutomaticRequestProcessingExceptForDataFlush - Deprecated. Use ABKManualRequestProcessing. * ABKManualRequestProcessing - The same as ABKAutomaticRequestProcessing, except that updates to * custom attributes and triggering of custom events will not automatically flush to the server. Instead, you * must call requestImmediateDataFlush when you want to synchronize newly updated user data with Braze. Note that * the configuration does not turn off all networking, i.e. requests important to the proper functionality of the Braze * SDK will still occur. * * Regardless of policy, Braze will intelligently combine requests on the request queue to minimize the total number of * requests and their combined payload. */ typedef NS_ENUM(NSInteger, ABKRequestProcessingPolicy) { ABKAutomaticRequestProcessing, ABKManualRequestProcessing, ABKAutomaticRequestProcessingExceptForDataFlush __deprecated_enum_msg("ABKAutomaticRequestProcessingExceptForDataFlush is deprecated. Use ManualRequestProcessing.") = ABKManualRequestProcessing }; /*! * Internal enum used to report the SDK flavor being used. */ typedef NS_ENUM(NSInteger , ABKSDKFlavor) { UNITY = 1, REACT, CORDOVA, XAMARIN, FLUTTER, SEGMENT, MPARTICLE, TEALIUM }; typedef NS_OPTIONS(NSUInteger, ABKDeviceOptions) { ABKDeviceOptionNone = 0, ABKDeviceOptionResolution = (1 << 0), ABKDeviceOptionCarrier = (1 << 1), ABKDeviceOptionLocale = (1 << 2), ABKDeviceOptionModel = (1 << 3), ABKDeviceOptionOSVersion = (1 << 4), // Note: The ABKDeviceOptionIDFV allowlist key currently has no effect. // IDFV is read regardless of allowlist settings due to its // role as the primary device identifier within the Braze system. ABKDeviceOptionIDFV = (1 << 5), ABKDeviceOptionIDFA = (1 << 6), ABKDeviceOptionPushEnabled = (1 << 7), ABKDeviceOptionTimezone = (1 << 8), ABKDeviceOptionPushAuthStatus = (1 << 9), ABKDeviceOptionAdTrackingEnabled = (1 << 10), ABKDeviceOptionPushDisplayOptions = (1 << 11), ABKDeviceOptionAll = ~ABKDeviceOptionNone }; /*! * Possible channels supported by the SDK. */ typedef NS_ENUM(NSInteger, ABKChannel) { ABKPushNotificationChannel, ABKInAppMessageChannel, ABKNewsFeedChannel, ABKContentCardChannel, ABKUnknownChannel __deprecated_enum_msg("ABKUnknownChannel will be removed in a future update.") }; /* * Braze Public API: Appboy */ @interface Appboy : NSObject /* ------------------------------------------------------------------------------------------------------ * Initialization */ /*! * Get the Appboy singleton. Returns nil if accessed before startWithApiKey: called. */ + (nullable Appboy *)sharedInstance; /*! * Get the Appboy singleton. Throws an exception if accessed before startWithApiKey: is called. */ + (nonnull Appboy *)unsafeInstance; /*! * @param apiKey The app's API key * @param application the current app * @param launchOptions The options NSDictionary that you get from application:didFinishLaunchingWithOptions * * @discussion Starts up Braze and tells it that your app is done launching. You should call this * method in your App Delegate application:didFinishLaunchingWithOptions method before calling makeKeyAndVisible, * accessing [Appboy sharedInstance] or otherwise rendering Braze view controllers. Your apiKey comes from * the Braze dashboard where you registered your app. */ + (void)startWithApiKey:(NSString *)apiKey inApplication:(UIApplication *)application withLaunchOptions:(nullable NSDictionary *)launchOptions; /*! * @param apiKey The app's API key * @param application The current app * @param launchOptions The options NSDictionary that you get from application:didFinishLaunchingWithOptions * @param appboyOptions An optional NSDictionary with startup configuration values for Braze. See below * for more information. * * @discussion Starts up Braze and tells it that your app is done launching. You should call this * method in your App Delegate application:didFinishLaunchingWithOptions method before calling makeKeyAndVisible, * accessing [Appboy sharedInstance] or otherwise rendering Braze view controllers. Your apiKey comes from * the Braze dashboard where you registered your app. */ + (void)startWithApiKey:(NSString *)apiKey inApplication:(UIApplication *)application withLaunchOptions:(nullable NSDictionary *)launchOptions withAppboyOptions:(nullable NSDictionary *)appboyOptions; /* ------------------------------------------------------------------------------------------------------ * Properties */ /*! * The current app user. * See ABKUser.h and changeUser:userId below. */ @property (readonly) ABKUser *user; @property (readonly) ABKFeedController *feedController; @property (readonly) ABKContentCardsController *contentCardsController; /*! * The policy regarding processing of network requests by the SDK. See the enumeration values for more information on * possible options. This value can be set at runtime, or can be injected in at startup via the appboyOptions dictionary. * * Any time the request processing policy is set to manual, any scheduled flush of the queue is canceled, but if the * request queue was already processing, the current queue will finish processing. If you need to cancel in flight * requests, you need to call
[[Appboy sharedInstance] shutdownServerCommunication]
. * * Setting the request policy does not automatically cause a flush to occur, it just allows for a flush to be scheduled * the next time an eligible request is enqueued. To force an immediate flush after changing the request processing * policy, invoke
[[Appboy sharedInstance] requestImmediateDataFlush]
. */ @property ABKRequestProcessingPolicy requestProcessingPolicy; /*! * A class extending ABKIDFADelegate can be set to provide the IDFA to Braze. */ @property (nonatomic, strong, nullable) id idfaDelegate; /*! * A class conforming to ABKSdkAuthenticationDelegate can be set to handle SDK Authentication errors. */ @property (nonatomic, strong, nullable) id sdkAuthenticationDelegate; /*! * A custom `NSURLSessionConfiguration` for configuring network session parameters. */ @property (nonatomic, readonly) NSURLSessionConfiguration *urlSessionConfiguration; #if !TARGET_OS_TV /*! * The current in-app message manager. * See ABKInAppMessageController.h. */ @property (readonly) ABKInAppMessageController *inAppMessageController; /*! * The Braze location manager provides access to location related functionality in the Braze SDK. * See ABKLocationManager.h. */ @property (nonatomic, readonly) ABKLocationManager *locationManager; /*! * A class conforming to the ABKURLDelegate protocol can be set to handle URLs in a custom way. */ @property (nonatomic, weak, nullable) id appboyUrlDelegate; /*! * A class conforming to ABKImageDelegate can be set to use a custom image library. */ @property (nonatomic, strong, nullable) id imageDelegate; /*! * Property for internal reporting of SDK flavor. */ @property (nonatomic) ABKSDKFlavor sdkFlavor; #endif /* ------------------------------------------------------------------------------------------------------ * Methods */ /*! * Enqueues a data flush request for the current user and immediately starts processing the network queue. Note that if * the queue already contains another request for the current user, that the new data flush request * will be merged into the already existing request and only one will execute for that user. * * If you're using ABKManualRequestProcessing, you only need to call this when you want to force * an immediate flush of updated user data. */ - (void)requestImmediateDataFlush; - (void)flushDataAndProcessRequestQueue __deprecated_msg("Please use `requestImmediateDataFlush` instead."); /*! * Stops all in flight server communication and enables manual request processing control to ensure that no automatic * network activity occurs. You should usually only call shutdownServerCommunication if the OS is forcing you to stop * background tasks upon exit of your application. To continue normal operation after calling this, you will need to * explicitly set the request processing mode back to your desired state. */ - (void)shutdownServerCommunication; /*! * @param userId The new user's ID (from the host application). * * @discussion * This method changes the user's ID. These user IDs should be private and not easily obtained (e.g. not a plain * email address or username). * * When you first start using Braze on a device, the user is considered "anonymous". You can use this method to * optionally identify a user with a unique ID, which enables the following: * * - If the same user is identified on another device, their user profile, usage history and event history will * be shared across devices. * * - If your app is used by multiple people, you can assign each of them a unique identifier to track them * separately. Only the most recent user on a particular device will receive push notifications and in-app * messages. * * - If you identify a user which has never been identified on another device, the entire history of that user as * an "anonymous" user on this device will be preserved and associated with the newly identified user. * * - However, if you identify a user which *has* been identified on another device, the previous anonymous * history of the user on this device will not be added to the already existing profile for that user. * * - Note that switching from one an anonymous user to an identified user or from one identified user to another is * a relatively costly operation. When you request the * user switch, the current session for the previous user is automatically closed and a new session is started. * Braze will also automatically make a data refresh request to get the news feed, in-app message and other information * for the new user. * * Note: Once you identify a user, you cannot go back to the "anonymous" profile. The transition from anonymous * to identified tracking only happens once because the initial anonymous user receives special treatment * to allow for preservation of their history. We recommend against changing the user id just because your app * has entered a "logged out" state because it separates this device from the user profile and thus you will be * unable to target the previously logged out user with re-engagement campaigns. If you anticipate multiple * users on the same device, but only want to target one of them when your app is in a logged out state, we recommend * separately keeping track of the user ID you want to target while logged out and switching back to * that user ID as part of your app's logout process. */ - (void)changeUser:(NSString *)userId; /*! * @param userId The new user's ID (from the host application) * @param signature The SDK Authentication signature for the user being identified. * * @discussion See documantation for `changeUser:` above */ - (void)changeUser:(NSString *)userId sdkAuthSignature:(nullable NSString *)signature; /*! * @param signature The SDK Authentication signature for the current user * * @discussion Sets the signature used for SDK authentication for the current user. */ - (void)setSdkAuthenticationSignature:(NSString *)signature; /*! * @discussion Unsubscribe from SDK Authentication errors. After this method is called, * the ABKSdkAuthenticationDelegate method `handleSdkAuthenticationError:` will not be called in the event of * an SDK Authentication error. */ - (void)unsubscribeFromSdkAuthenticationErrors; /*! * @param eventName The name of the event to log. * * @discussion Adds an app specific event to event tracking log that's lazily pushed up to the server. Think of * events like counters. That is, each time you log an event, we'll update a counter for that user. Events should be * fairly broad like "beat level 1" or "watched video" instead of something more specific like "watched Katy * Perry's Last Friday Night" so you can create more broad user segments for targeting. * *
 * [[Appboy sharedInstance] logCustomEvent:@"clicked_button"];
 * 
*/ - (void)logCustomEvent:(NSString *)eventName; /*! * @param eventName The name of the event to log. * @param properties An NSDictionary of properties to associate with this purchase. Property keys are non-empty NSString objects with * <= 255 characters and no leading dollar signs. Property values can be NSNumber booleans, integers, floats < 62 bits, NSDate objects, * NSString objects with <= 255 characters, or any JSON Encodable object including NSArray and NSDictionary of the previous data types (nested properties). Total length of encoded properties must be under 50 KB. * * @discussion Adds an app specific event to event tracking log that's lazily pushed up to the server. Think of * events like counters. That is, each time you log an event, we'll update a counter for that user. Events should be * fairly broad like "beat level 1" or "watched video" instead of something more specific like "watched Katy * Perry's Last Friday Night" so you can create more broad user segments for targeting. * *
 * [[Appboy sharedInstance] logCustomEvent:@"clicked_button" properties:@{@"key1":@"val"}];
 * 
*/ - (void)logCustomEvent:(NSString *)eventName withProperties:(nullable NSDictionary *)properties; /*! * This method is equivalent to calling logPurchase:inCurrency:atPrice:withQuantity:andProperties: with a quantity of 1 and nil properties. * Please see logPurchase:inCurrency:atPrice:withQuantity:andProperties: for more information. */ - (void)logPurchase:(NSString *)productIdentifier inCurrency:(NSString *)currencyCode atPrice:(NSDecimalNumber *)price; /*! * This method is equivalent to calling logPurchase:inCurrency:atPrice:withQuantity:andProperties with a quantity of 1. * Please see logPurchase:inCurrency:atPrice:withQuantity:andProperties: for more information. */ - (void)logPurchase:(NSString *)productIdentifier inCurrency:(NSString *)currencyCode atPrice:(NSDecimalNumber *)price withProperties:(nullable NSDictionary *)properties; /*! * This method is equivalent to calling logPurchase:inCurrency:atPrice:withQuantity:andProperties with nil properties. * Please see logPurchase:inCurrency:atPrice:withQuantity:andProperties: for more information. */ - (void)logPurchase:(NSString *)productIdentifier inCurrency:(NSString *)currencyCode atPrice:(NSDecimalNumber *)price withQuantity:(NSUInteger)quantity; /*! * @param productIdentifier A String indicating the product that was purchased. Usually the product identifier in the * iTunes store. * @param currencyCode Currencies should be represented as an ISO 4217 currency code. Prices should * be sent in decimal format, with the same base units as are provided by the SKProduct class. Callers of this method * who have access to the NSLocale object for the purchase in question (which can be obtained from SKProduct listings * provided by StoreKit) can obtain the currency code by invoking: *
[locale objectForKey:NSLocaleCurrencyCode]
* Supported currency symbols include: AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BHD, BIF, * BMD, BND, BOB, BRL, BSD, BTC, BTN, BWP, BYR, BZD, CAD, CDF, CHF, CLF, CLP, CNY, COP, CRC, CUC, CUP, CVE, CZK, DJF, * DKK, DOP, DZD, EEK, EGP, ERN, ETB, EUR, FJD, FKP, GBP, GEL, GGP, GHS, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, * IDR, ILS, IMP, INR, IQD, IRR, ISK, JEP, JMD, JOD, JPY, KES, KGS, KHR, KMF, KPW, KRW, KWD, KYD, KZT, LAK, LBP, LKR, LRD, * LSL, LTL, LVL, LYD, MAD, MDL, MGA, MKD, MMK, MNT, MOP, MRO, MTL, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, * NZD, OMR, PAB, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RUB, RWF, SAR, SBD, SCR, SDG, SEK, SGD, SHP, SLL, SOS, SRD, * STD, SVC, SYP, SZL, THB, TJS, TMT, TND, TOP, TRY, TTD, TWD, TZS, UAH, UGX, USD, UYU, UZS, VEF, VND, VUV, WST, XAF, XAG, * XAU, XCD, XDR, XOF, XPD, XPF, XPT, YER, ZAR, ZMK, ZMW and ZWL. Any other provided currency symbol will result in a logged * warning and no other action taken by the SDK. * @param price Prices should be reported as NSDecimalNumber objects. Base units are treated the same as with SKProduct * from StoreKit and depend on the currency. As an example, USD should be reported as Dollars.Cents, whereas JPY should * be reported as a whole number of Yen. All provided NSDecimalNumber values will have NSRoundPlain rounding applied * such that a maximum of two digits exist after their decimal point. * @param quantity An unsigned number to indicate the purchase quantity. This number must be greater than 0 but no larger than 100. * @param properties An NSDictionary of properties to associate with this purchase. Property keys are non-empty NSString objects with * <= 255 characters and no leading dollar signs. Property values can be NSNumber integers, floats, booleans < 62 bits in length, NSDate objects or * NSString objects with <= 255 characters. * * @discussion Logs a purchase made in the application. * * Note: Braze supports purchases in multiple currencies. Purchases that you report in a currency other than USD will * be shown in the dashboard in USD based on the exchange rate at the date they were reported. */ - (void)logPurchase:(NSString *)productIdentifier inCurrency:(NSString *)currencyCode atPrice:(NSDecimalNumber *)price withQuantity:(NSUInteger)quantity andProperties:(nullable NSDictionary *)properties; /*! * If you're displaying cards on your own instead of using ABKFeedViewController, you should still report impressions of * the news feed back to Braze with this method so that your campaign reporting features still work in the dashboard. */ - (void)logFeedDisplayed; /*! * If you're displaying content cards on your own instead of using ABKContentCardsViewController, you should still report * impressions of the content cards back to Braze with this method so that your campaign reporting features still work * in the dashboard. */ - (void)logContentCardsDisplayed; /*! * Enqueues a news feed request for the current user. Note that if the queue already contains another request for the * current user, that the new feed request will be merged into the already existing request and only one will execute * for that user. * * When the new cards for news feed return from Braze server, the SDK will post an ABKFeedUpdatedNotification with an * ABKFeedUpdatedIsSuccessfulKey in the notification's userInfo dictionary to indicate if the news feed request is successful * or not. For more detail about the ABKFeedUpdatedNotification and the ABKFeedUpdatedIsSuccessfulKey, please check ABKFeedController. */ - (void)requestFeedRefresh; /*! * Enqueues a content cards request for the current user. */ - (void)requestContentCardsRefresh; /*! * Manually request geofences with a specific location. */ - (void)requestGeofencesWithLongitude:(double)longitude latitude:(double)latitude; /*! * Get the device ID - the IDFV - which will reset if all apps for a given vendor are removed from the device. * * @return The device ID. */ - (NSString *)getDeviceId; #if !TARGET_OS_TV /*! * @param deviceToken The device's push token. * * @discussion This method posts a token to Braze servers to associate the token with the current device. */ - (void)registerDeviceToken:(NSData *)deviceToken; /*! * @param application The app's UIApplication object * @param notification An NSDictionary passed in from the didReceiveRemoteNotification call * * @discussion This method forwards remote notifications to Braze. Call it from the application:didReceiveRemoteNotification * method of your App Delegate. */ - (void)registerApplication:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification NS_DEPRECATED_IOS(3_0, 10_0, "`registerApplication:didReceiveRemoteNotification:` is deprecated in iOS 10, please use `registerApplication:didReceiveRemoteNotification:fetchCompletionHandler:` instead."); /*! * @param application The app's UIApplication object * @param notification An NSDictionary passed in from the didReceiveRemoteNotification:fetchCompletionHandler: call * @param completionHandler A block passed in from the didReceiveRemoteNotification:fetchCompletionHandler: call * * @discussion This method forwards remote notifications to Braze. If the completionHandler is passed in when * the method is called, Braze will call the completionHandler. However, if the completionHandler is not passed in, * it is the host app's responsibility to call the completionHandler. * Call it from the application:didReceiveRemoteNotification:fetchCompletionHandler: method of your App Delegate. */ - (void)registerApplication:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification fetchCompletionHandler:(nullable void (^)(UIBackgroundFetchResult))completionHandler; /*! * @param identifier The action identifier passed in from the handleActionWithIdentifier:forRemoteNotification:. * @param userInfo An NSDictionary passed in from the handleActionWithIdentifier:forRemoteNotification: call. * @param completionHandler A block passed in from the didReceiveRemoteNotification:fetchCompletionHandler: call * * @discussion This method forwards remote notifications and the custom action chosen by user to Braze. Call it from * the application:handleActionWithIdentifier:forRemoteNotification: method of your App Delegate. */ - (void)getActionWithIdentifier:(NSString *)identifier forRemoteNotification:(NSDictionary *)userInfo completionHandler:(nullable void (^)(void))completionHandler NS_DEPRECATED_IOS(8_0, 10_0,"`getActionWithIdentifier:forRemoteNotification:completionHandler:` is deprecated in iOS 10, please use `userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:` instead."); /*! * @param center The app's current UNUserNotificationCenter object * @param response The UNNotificationResponse object passed in from the didReceiveNotificationResponse:withCompletionHandler: call * @param completionHandler A block passed in from the didReceiveNotificationResponse:withCompletionHandler: call. Braze will call * it at the end of the method if one is passed in. If you prefer to handle the completionHandler youself, please pass nil to Braze. * * @discussion This method forwards the response of the notification to Braze after user interacted with the notification. * Call it from the userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler: method of your App Delegate. */ - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(nullable void (^)(void))completionHandler NS_AVAILABLE_IOS(10_0); /*! * @param pushAuthGranted The boolean value passed in from completionHandler in UNUserNotificationCenter's * requestAuthorizationWithOptions:completionHandler: method, which indicates if the push authorization * was granted or not. * * @discussion This method forwards the push authorization result to Braze after the user interacts with * the notification prompt. * Call it from the UNUserNotificationCenter's requestAuthorizationWithOptions:completionHandler: method * when you prompt users to enable push. */ - (void)pushAuthorizationFromUserNotificationCenter:(BOOL)pushAuthGranted; #endif /*! * Adds SDK Metadata values to those automatically collected by the SDK. * * Metadata tell Braze how the SDK is integrated (e.g. wrapper, package manager, etc.) * * @param metadata The metadata values reflecting the current SDK integration. */ - (void)addSdkMetadata:(NSArray *)metadata; /* ------------------------------------------------------------------------------------------------------ * Data processing configuration methods. */ /*! * @discussion This method immediately wipes all data from the Braze iOS SDK. After this method is * called, the sharedInstance singleton will be nulled out and Braze functionality will be disabled * until the next call to startWithApiKey: in a subsequent app run. All references to the previous * singleton should be released. * * Note that the next call to startWithApiKey: must take place in a subsequent app run. Initializing the SDK * within the same app run after calling this method is not supported. * * The SDK will automatically re-enable itself when startWithApiKey: is called. There is * no need to call requestEnableSDKOnNextAppRun: to re-enable the SDK. wipeDataAndDisableForAppRun: * may be used at any time, including while the SDK is otherwise disabled. * * Note that if you are using unsafeInstance:, further calls to unsafeInstance: after using this * method will cause an uncaught exception to be thrown. We do not recommend using this method in * concert with unsafeInstance:. */ + (void)wipeDataAndDisableForAppRun; /*! * @discussion This method immediately disables the Braze iOS SDK. After this method is called, the * sharedInstance singleton will be nulled out and Braze functionality will be disabled until the * SDK is re-enabled via a call to requestEnableSDKOnNextAppRun: and re-initialized in a subsequent * app run via a call to startWithApiKey:. All references to the previous singleton should be released. * * Note that the next call to startWithApiKey: must take place in a subsequent app run. Initializing the SDK * within the same app run after calling this method is not supported. * * Unlike with wipeDataAndDisableForAppRun:, calling requestEnableSDKOnNextAppRun: is required to * re-enable the SDK after the method is called. * * Note that if you are using unsafeInstance:, further calls to unsafeInstance: after using this * method will cause an exception to be thrown. We do not recommend using this method in concert * with unsafeInstance:. */ + (void)disableSDK; /*! * @discussion This method requests the Braze iOS SDK to be re-enabled on the next app run. * After this method is called, the following call to startWithApiKey: will successfully * re-enable the SDK. Braze functionality will remain disabled until that point. * * Note that this method does not re-initialize the Appboy singleton on its own nor re-enable * Braze functionality immediately. */ + (void)requestEnableSDKOnNextAppRun; @end NS_ASSUME_NONNULL_END ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Headers/AppboyKit.h ================================================ #import "Appboy.h" #import "ABKUser.h" #import "ABKFacebookUser.h" #import "ABKTwitterUser.h" #import "ABKAttributionData.h" // Cards #import "ABKCard.h" #import "ABKBannerCard.h" #import "ABKCaptionedImageCard.h" #import "ABKClassicCard.h" #import "ABKTextAnnouncementCard.h" // Content Card #import "ABKContentCard.h" #import "ABKBannerContentCard.h" #import "ABKCaptionedImageContentCard.h" #import "ABKClassicContentCard.h" // SDK Authentication #import "ABKSdkAuthenticationError.h" #import "ABKSdkAuthenticationDelegate.h" #if !TARGET_OS_TV // In-app Message #import "ABKInAppMessage.h" #import "ABKInAppMessageSlideup.h" #import "ABKInAppMessageImmersive.h" #import "ABKInAppMessageModal.h" #import "ABKInAppMessageFull.h" #import "ABKInAppMessageHTML.h" #import "ABKInAppMessageHTMLFull.h" #import "ABKInAppMessageHTMLBase.h" #import "ABKInAppMessageControl.h" #import "ABKInAppMessageControllerDelegate.h" #import "ABKInAppMessageController.h" #import "ABKInAppMessageButton.h" #import "ABKInAppMessageWebViewBridge.h" #import "ABKInAppMessageUIControlling.h" #import "ABKInAppMessageDarkTheme.h" #import "ABKInAppMessageDarkButtonTheme.h" // News Feed #import "ABKFeedController.h" // Content Cards Feed #import "ABKContentCardsController.h" // IDFA #import "ABKIDFADelegate.h" // SDWebImage #import "ABKSDWebImageProxy.h" // ABKImageDelegate #import "ABKImageDelegate.h" // Location #import "ABKLocationManager.h" #import "ABKLocationManagerProvider.h" #import "ABKURLDelegate.h" #import "ABKPushUtils.h" #import "ABKModalWebViewController.h" #import "ABKNoConnectionLocalization.h" #endif ================================================ FILE: Appboy-tvOS-SDK/AppboyTVOSKit.framework/Modules/module.modulemap ================================================ framework module AppboyTVOSKit { umbrella header "AppboyKit.h" export * module * { export * } } ================================================ FILE: Appboy-tvOS-SDK.podspec ================================================ Pod::Spec.new do |s| s.name = "Appboy-tvOS-SDK" s.version = "4.7.0" s.summary = "This is the Braze tvOS SDK for Mobile Marketing Automation" s.homepage = "http://www.braze.com" s.license = { :type => 'Commercial', :text => 'Please refer to https://github.com/Appboy/appboy-ios-sdk/blob/master/LICENSE'} s.author = { "Appboy" => "http://www.braze.com" } s.source = { :git => 'https://github.com/Appboy/appboy-ios-sdk.git', :tag => s.version.to_s} s.platform = :tvos s.tvos.deployment_target = 11.0 s.requires_arc = true s.documentation_url = 'https://www.braze.com/docs' s.tvos.frameworks = 'SystemConfiguration' s.preserve_paths = 'Appboy-tvOS-SDK/AppboyTVOSKit.framework' s.vendored_frameworks = 'Appboy-tvOS-SDK/AppboyTVOSKit.framework' # Skip this architecture to pass Pod validation since we removed the `arm64` simulator ARCH in order to use lipo later s.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=appletvsimulator*]' => 'arm64' } end ================================================ FILE: AppboyKit/ABKLocationManagerProvider.m ================================================ #import "ABKLocationManagerProvider.h" #if !TARGET_OS_TV #import #endif @implementation ABKLocationManagerProvider + (BOOL)locationServicesEnabled { #if !TARGET_OS_TV return YES; #endif return NO; } @end ================================================ FILE: AppboyKit/ABKModalWebViewController.m ================================================ #import "ABKModalWebViewController.h" #import "ABKNoConnectionLocalization.h" static NSString *const titleKeyPath = @"title"; static NSString *const estimatedProgressKeyPath = @"estimatedProgress"; static NSString *const localizedNoConnectionKey = @"Appboy.no-connection.message"; @implementation ABKModalWebViewController - (void)viewDidLoad { [super viewDidLoad]; UIViewController *webViewController = [[UIViewController alloc] init]; self.webView = [self getWebView]; webViewController.view = self.webView; #if !TARGET_OS_TV if (@available(iOS 15.0, *)) { self.view.backgroundColor = UIColor.systemGroupedBackgroundColor; } #endif [self setupProgressBarWithViewController:webViewController]; UIBarButtonItem *closeBarButton = [self getDoneBarButtonItem]; [webViewController.navigationItem setRightBarButtonItem:closeBarButton]; [self.webView addObserver:self forKeyPath:titleKeyPath options:NSKeyValueObservingOptionNew context:nil]; [self.webView addObserver:self forKeyPath:estimatedProgressKeyPath options:NSKeyValueObservingOptionNew context:nil]; [self setViewControllers:@[webViewController]]; [self.webView loadRequest:[NSURLRequest requestWithURL:self.url]]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([titleKeyPath isEqualToString:keyPath]) { self.title = self.webView.title; } else if ([estimatedProgressKeyPath isEqualToString:keyPath]) { if (self.webView.estimatedProgress == 1.0) { [UIView animateWithDuration:1 animations:^{ self.progressBar.alpha = 0.0; }]; } else if (self.webView.estimatedProgress < 1.0) { self.progressBar.alpha = 1.0; [self.progressBar setProgress:self.webView.estimatedProgress animated:YES]; } } } - (void)dealloc { [self.webView removeObserver:self forKeyPath:titleKeyPath]; [self.webView removeObserver:self forKeyPath:estimatedProgressKeyPath]; } #pragma mark - Customization Methods /*! * @discussion Returns a WKWebView object, whose navigationDelegate is this ABKModalWebViewController instance. * * If you want to do any customization to the WKWebView, please override this method in an ABKModalWebViewController * category and return the customized WKWebView. All instances of ABKModalWebViewController will then * call the category's `getWebView` implementation instead of this method. * */ - (WKWebView *)getWebView { WKWebViewConfiguration *webViewConfiguration = [[WKWebViewConfiguration alloc] init]; webViewConfiguration.allowsInlineMediaPlayback = YES; WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:webViewConfiguration]; webView.navigationDelegate = self; return webView; } /*! * * @param viewController The view controller to which the progress bar will be added as a subview. * * @discussion Creates a UIProgressView and puts it on top of the param viewController. * * If you want to do any customization to the progress bar, please override this method in an ABKModalWebViewController * category and set up the progress bar. All instances of ABKModalWebViewController will then * call the category's `setupProgressBarWithViewController:` implementation instead of this method. * */ - (void)setupProgressBarWithViewController:(UIViewController *)viewController { UIProgressView *progressBar = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleBar]; progressBar.alpha = 0; self.progressBar = progressBar; [viewController.view addSubview:self.progressBar]; self.progressBar.translatesAutoresizingMaskIntoConstraints = NO; [viewController.view addConstraint:[NSLayoutConstraint constraintWithItem:self.progressBar attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:viewController.topLayoutGuide attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0]]; [viewController.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[progressBar]|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:@{@"progressBar" : self.progressBar}]]; } /*! * @discussion Returns the Done UIBarButtonItem, which allows the user to dismiss the modal web view. * * If you want to do any customization to the Done button, please override this method in an ABKModalWebViewController * category and return the customized UIBarButtonItem. All instances of ABKModalWebViewController will then * call the category's `getDoneBarButtonItem` implementation instead of this method. * */ - (UIBarButtonItem *)getDoneBarButtonItem { return [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(closeButtonPressed:)]; } - (void)closeButtonPressed:(id)sender { [self dismissViewControllerAnimated:YES completion:nil]; } #pragma mark - WKNavigationDelegate methods - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { NSString *urlString = [[navigationAction.request.mainDocumentURL absoluteString] lowercaseString]; NSArray *stringComponents = [urlString componentsSeparatedByString:@":"]; if ([stringComponents[1] hasPrefix:@"//itunes.apple.com"] || (![stringComponents[0] isEqual:@"http"] && ![stringComponents[0] isEqual:@"https"])) { // Dismiss the modal web view and let the system handle the deep links if ([[UIApplication sharedApplication] openURL:navigationAction.request.URL]) { decisionHandler(WKNavigationActionPolicyCancel); [self dismissViewControllerAnimated:YES completion:nil]; return; } } decisionHandler(WKNavigationActionPolicyAllow); } - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { self.progressBar.alpha = 0.0; } - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error { self.progressBar.alpha = 0.0; // Display localized "No Connection" message UILabel *label = [[UILabel alloc] init]; label.textAlignment = NSTextAlignmentCenter; label.numberOfLines = 0; NSString *localizedNoConectionMessage = NSLocalizedString(@"Appboy.no-connection.message", @"No connection error message for URL loading failure"); if (localizedNoConectionMessage.length == 0 || [localizedNoConnectionKey isEqualToString:localizedNoConectionMessage]) { localizedNoConectionMessage = [ABKNoConnectionLocalization getNoConnectionLocalizedString]; } label.text = localizedNoConectionMessage; [self.webView addSubview:label]; label.translatesAutoresizingMaskIntoConstraints = NO; [self.webView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-10-[noConnectionLabel]-10-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:@{@"noConnectionLabel" : label}]]; [self.webView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[noConnectionLabel]|" options:NSLayoutFormatAlignAllCenterY metrics:nil views:@{@"noConnectionLabel" : label}]]; } @end ================================================ FILE: AppboyKit/ABKNoConnectionLocalization.m ================================================ #import "ABKNoConnectionLocalization.h" @implementation ABKNoConnectionLocalization + (NSDictionary *)localizedNoConnectionStringDictionary { return @{@"ar":@"لا يمكن إجراء الاتصال بالشبكة. يرجى تكرار المحاولة لاحقا.", @"da":@"Kan ikke etablere netværksforbindelse. Prøv venligst senere.", @"de":@"Netzwerkverbindung kann nicht aufgebaut werden. Bitte später noch einmal versuchen.", @"en":@"Cannot establish network connection. Please try again later.", @"es-419":@"No se puede establecer conexión con la red. Por favor, vuelva a intentarlo más tarde.", @"es-MX":@"No se puede establecer conexión con la red. Por favor, vuelva a intentarlo más tarde.", @"es":@"No se puede establecer conexión de red. Por favor inténtelo más tarde.", @"et":@"Võrguühenduse loomine ebaõnnestus. Palun proovige hiljem uuesti.", @"fi":@"Verkkoyhteyttä ei voida luoda. Yritä myöhemmin uudelleen.", @"fil":@"Hindi makapagtatag ng koneksyon sa network. angyaring subukan muli mamaya.", @"fr":@"Impossible d'établir la connexion réseau. Veuillez réessayer ultérieurement.", @"he":@".לא ניתן לקבוע חיבור רשת.בבקשה נסה שוב בקרוב", @"hi":@"नेटवर्क कनेक्शन स्थापित नहीं हो रहा है।. कृपया बाद में दोबारा प्रयास करें।.", @"id":@"Tidak bisa melakukan koneksi jaringan. Coba lagi nanti.", @"it":@"Impossibile stabilire una connessione di rete. Riprovare più tardi.", @"ja":@"ネットワークに接続できません。後でもう一度試してください。", @"km":@"មិនអាចបង្កើតបណ្តាញតភ្ជាប់បានទេ. សូមព្យាយាមម្តងទៀតនៅពេលក្រោយ.", @"ko":@"네트워크 연결을 할 수 없습니다. 나중에 다시 시도해 주십시오.", @"lo":@"ບໍ່​ສາ​ມາດ​ຕັ້ງ​ການ​ເຊື່ອມ​ຕໍ່​ເຄືອ​ຂ່າຍ​ໄດ້. ກະ​ລຸ​ນາ​ລອງ​ໃໝ່​ພາຍ​ຫຼັງ.", @"ms":@"Tidak boleh membuat sambungan rangkaian. Sila cuba kemudian.", @"my":@"ကြန္ယက္ဆက္သြယ္ျခင္း မျပဳလုပ္ႏိုင္ပါ။. ေက်းဇူးျပဳ၍ ထပ္မံၾကိဳးစားၾကည္႕ပါ။.", @"nb":@"Kan ikke etablere nettverkstilkobling. Vennligst prøv igjen senere.", @"nl":@"Kan geen netwerkverbinding maken. Probeer het later opnieuw.", @"pl":@"Nie można ustanowić połączenia z siecią. Proszę spróbować ponownie później.", @"pt-PT":@"Não é possível estabelecer a ligação à rede. Por favor, tente mais tarde.", @"pt":@"Não é possível estabelecer uma conexão de rede. Tente novamente mais tarde.", @"ru":@"Невозможно установить сетевое подключение. Пожалуйста, повторите попытку позже.", @"sv":@"Det gick inte att skapa en nätverksanslutning. Försök igen senare.", @"th":@"ไม่สามารถสร้างการเชื่อมต่อเครือข่าย. กรุณาลองใหม่ภายหลัง.", @"vi":@"Không thể thiết lập kết nối mạng. Vui lòng thử lại sau.", @"zh-Hans":@"无法建立网络连接。请稍候再试。", @"zh-Hant":@"無法建立網路連線。請稍候再試。", @"zh-HK":@"無法建立網路連線。請稍候再試。", @"zh-TW":@"無法建立網路連線。請稍候再試。", @"zh":@"无法建立网络连接。请稍候再试。"}; } + (NSString *)getNoConnectionLocalizedString { NSString *language = [[NSLocale preferredLanguages] count]? [NSLocale preferredLanguages][0]: @"en"; NSDictionary *localizedStringDict = [self localizedNoConnectionStringDictionary]; while (localizedStringDict[language] == nil && [language rangeOfString:@"-"].location != NSNotFound) { NSArray *languageComponent = [language componentsSeparatedByString:@"-"]; language = [[languageComponent subarrayWithRange:NSMakeRange(0, languageComponent.count - 1)] componentsJoinedByString:@"-"]; } NSString *localizedString = localizedStringDict[language] ? localizedStringDict[language] : localizedStringDict[@"en"]; return localizedString; } @end ================================================ FILE: AppboyKit/ABKSDWebImageProxy.m ================================================ #import "ABKSDWebImageProxy.h" #import #import #import #import @implementation ABKSDWebImageProxy + (void)setImageForView:(UIImageView *)imageView showActivityIndicator:(BOOL)showActivityIndicator withURL:(nullable NSURL *)imageURL imagePlaceHolder:(nullable UIImage *)placeHolder completed:(nullable void (^)(UIImage * _Nullable image, NSError * _Nullable error, NSInteger cacheType, NSURL * _Nullable imageURL))completion { if (showActivityIndicator) { imageView.sd_imageIndicator = SDWebImageActivityIndicator.grayIndicator; } [imageView sd_setImageWithURL:imageURL placeholderImage:placeHolder options: (SDWebImageQueryMemoryData | SDWebImageQueryDiskDataSync) completed:completion]; } + (void)loadImageWithURL:(nullable NSURL *)url options:(NSInteger)options completed:(nullable void (^)(UIImage *image, NSData *data, NSError *error, NSInteger cacheType, BOOL finished, NSURL *imageURL))completion { [[SDWebImageManager sharedManager] loadImageWithURL:url options:options progress:nil completed:completion]; } + (void)diskImageExistsForURL:(nullable NSURL *)url completed:(nullable void (^)(BOOL isInCache))completion{ if (url != nil) { [[SDImageCache sharedImageCache] diskImageExistsWithKey:url.absoluteString completion:completion]; } } + (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url { return [[SDWebImageManager sharedManager] cacheKeyForURL:url]; } + (void)removeSDWebImageForKey:(nullable NSString *)key { [[SDImageCache sharedImageCache] removeImageForKey:key withCompletion:nil]; } + (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key { return [[SDImageCache sharedImageCache] imageFromCacheForKey:key]; } + (void)clearSDWebImageCache { [[SDImageCache sharedImageCache] clearMemory]; [[SDImageCache sharedImageCache] clearDiskOnCompletion:nil]; } + (BOOL)isSupportedSDWebImageVersion { BOOL imageViewMethodsExist = [UIImageView instancesRespondToSelector:@selector(setSd_imageIndicator:)] && [UIImageView instancesRespondToSelector:@selector(sd_setImageWithURL:placeholderImage:completed:)]; SDWebImageManager *imageManager = [SDWebImageManager sharedManager]; BOOL managerMethodsExist = [imageManager respondsToSelector:@selector(loadImageWithURL:options:progress:completed:)] && [imageManager respondsToSelector:@selector(cacheKeyForURL:)]; SDImageCache *imageCache = [SDImageCache sharedImageCache]; BOOL imageCacheMethodsExist = [imageCache respondsToSelector:@selector(removeImageForKey:withCompletion:)] && [imageCache respondsToSelector:@selector(clearDiskOnCompletion:)] && [imageCache respondsToSelector:@selector(diskImageExistsWithKey:completion:)] && [imageCache respondsToSelector:@selector(clearMemory)] && [imageCache respondsToSelector:@selector(imageFromCacheForKey:)]; return imageViewMethodsExist && managerMethodsExist && imageCacheMethodsExist; } @end ================================================ FILE: AppboyKit/Appboy.bundle/PrivacyInfo.xcprivacy ================================================ NSPrivacyTracking NSPrivacyTrackingDomains NSPrivacyAccessedAPITypes NSPrivacyAccessedAPIType NSPrivacyAccessedAPICategoryUserDefaults NSPrivacyAccessedAPITypeReasons CA92.1 NSPrivacyAccessedAPIType NSPrivacyAccessedAPICategoryFileTimestamp NSPrivacyAccessedAPITypeReasons C617.1 NSPrivacyCollectedDataTypes NSPrivacyCollectedDataType NSPrivacyCollectedDataTypeUserID NSPrivacyCollectedDataTypeLinked NSPrivacyCollectedDataTypeTracking NSPrivacyCollectedDataTypePurposes NSPrivacyCollectedDataTypePurposeProductPersonalization NSPrivacyCollectedDataTypePurposeAnalytics NSPrivacyCollectedDataTypePurposeAppFunctionality NSPrivacyCollectedDataType NSPrivacyCollectedDataTypeDeviceID NSPrivacyCollectedDataTypeLinked NSPrivacyCollectedDataTypeTracking NSPrivacyCollectedDataTypePurposes NSPrivacyCollectedDataTypePurposeProductPersonalization NSPrivacyCollectedDataTypePurposeAnalytics NSPrivacyCollectedDataTypePurposeAppFunctionality NSPrivacyCollectedDataType NSPrivacyCollectedDataTypeProductInteraction NSPrivacyCollectedDataTypeLinked NSPrivacyCollectedDataTypeTracking NSPrivacyCollectedDataTypePurposes NSPrivacyCollectedDataTypePurposeProductPersonalization NSPrivacyCollectedDataTypePurposeAnalytics NSPrivacyCollectedDataTypePurposeAppFunctionality NSPrivacyCollectedDataType NSPrivacyCollectedDataTypePreciseLocation NSPrivacyCollectedDataTypeLinked NSPrivacyCollectedDataTypeTracking NSPrivacyCollectedDataTypePurposes NSPrivacyCollectedDataTypePurposeProductPersonalization NSPrivacyCollectedDataTypePurposeAnalytics NSPrivacyCollectedDataType NSPrivacyCollectedDataTypeCoarseLocation NSPrivacyCollectedDataTypeLinked NSPrivacyCollectedDataTypeTracking NSPrivacyCollectedDataTypePurposes NSPrivacyCollectedDataTypePurposeProductPersonalization NSPrivacyCollectedDataTypePurposeAnalytics ================================================ FILE: AppboyKit/Appboy.bundle/ZipArchive_LICENSE.txt ================================================ Copyright (c) 2010-2015, ZipArchive, https://github.com/ZipArchive 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. ================================================ FILE: AppboyKit/Appboy.bundle/_CodeSignature/CodeResources ================================================ files PrivacyInfo.xcprivacy 35537WDlOfHuEOldTrUa7OmzNwQ= ZipArchive_LICENSE.txt JbmSdiuxe/Zs/esUY7pUvV4byF4= appboy-spm-cleanup.sh O3UfCL3lKKMB/+splkNXf6zYbbU= ar.lproj/LocalizedAppboyUIString.strings hash ikkSl37t+7dVXcdDSsplItVo5v4= optional cs.lproj/LocalizedAppboyUIString.strings hash HpBIYzzQP2b/gz2ASERTvoi95m4= optional da.lproj/LocalizedAppboyUIString.strings hash sg0g9Tgrlg3sJjwTl+G3PC8FAJg= optional de.lproj/LocalizedAppboyUIString.strings hash IoqO+svUlTLorH1d321C+DaCvLU= optional en.lproj/LocalizedAppboyUIString.strings hash CcyvcSMMNgJPpJScduQ1DvYCPqc= optional es-419.lproj/LocalizedAppboyUIString.strings hash LmBpUiGVEU/srcwSdgMN2xHVnsc= optional es-MX.lproj/LocalizedAppboyUIString.strings hash LmBpUiGVEU/srcwSdgMN2xHVnsc= optional es.lproj/LocalizedAppboyUIString.strings hash iAm4QTEmMOfm3uJbSU2CjYASqg4= optional et.lproj/LocalizedAppboyUIString.strings hash YRS0Fh60D+5z7QyzbRifH6lS5m0= optional fi.lproj/LocalizedAppboyUIString.strings hash ElciMeWd7+DdrGsUYIV1BCc9zts= optional fil.lproj/LocalizedAppboyUIString.strings hash AwAsH+O3Oum5GBbG9U/UzAHs+Uo= optional fr.lproj/LocalizedAppboyUIString.strings hash 4MoSFYmXS1rkntVmD8Bdc7Hn5pw= optional he.lproj/LocalizedAppboyUIString.strings hash Rp7mkbk7PXmWefbaSE8FDXGXles= optional hi.lproj/LocalizedAppboyUIString.strings hash O8MWNHc3Um4NcsIrID5Vk4dd54s= optional id.lproj/LocalizedAppboyUIString.strings hash 49qpfq7iRtfxxUbVInBDeKNjz7E= optional it.lproj/LocalizedAppboyUIString.strings hash laa/58WCKhmdGMlVjsTlBYUopG4= optional ja.lproj/LocalizedAppboyUIString.strings hash 3wDAz31yuk5UdZyTMdeUKUbgbeU= optional km.lproj/LocalizedAppboyUIString.strings hash PAlPrqyz9/Dr9FoyARSmLUUSNlY= optional ko.lproj/LocalizedAppboyUIString.strings hash itXcFANBAU96clWcX1rA7p30JnE= optional lo.lproj/LocalizedAppboyUIString.strings hash 9ummy0p0yIJpe1mMX7Xl03P0H/w= optional ms.lproj/LocalizedAppboyUIString.strings hash Gv7vjcrr6syw+oqHQ43wPMXrgNk= optional my.lproj/LocalizedAppboyUIString.strings hash 9yuPTyT4nWP0qa0okY7pGmudiV0= optional nb.lproj/LocalizedAppboyUIString.strings hash l80bnfoSc9rlRA4bXKJ0rwRC0OM= optional nl.lproj/LocalizedAppboyUIString.strings hash /h2VZ4vvhTv86/q3pb2e1Egu/Sw= optional pl.lproj/LocalizedAppboyUIString.strings hash BsEGyLvt3DQkwFvz66oYe7ua/Po= optional pt-PT.lproj/LocalizedAppboyUIString.strings hash 4TjlnSbVdiRFyVbE2qBiMd+CyOQ= optional pt.lproj/LocalizedAppboyUIString.strings hash zqP4s5SMAvHJJIetNS1R6ZE4O54= optional ru.lproj/LocalizedAppboyUIString.strings hash STrDNbH4mbH8IiQ0hn8Vek5WgLQ= optional sv.lproj/LocalizedAppboyUIString.strings hash mAlAT4rTioVuM3DYq5gih2kfI9o= optional th.lproj/LocalizedAppboyUIString.strings hash DeAcN0mnPKHFwDbYuGg+sr5xk+0= optional uk.lproj/LocalizedAppboyUIString.strings hash Tc4MJlZRNrNeCkLxxfRMq9EFW68= optional vi.lproj/LocalizedAppboyUIString.strings hash WnpBLIVxIeYHM1hAYdI5jgR/Y3o= optional zh-HK.lproj/LocalizedAppboyUIString.strings hash A4gTpmd3RAdTOb5x7jO211+TLu8= optional zh-Hans.lproj/LocalizedAppboyUIString.strings hash 6FRvJc2sxWJf+sECiTmCMl2VFOQ= optional zh-Hant.lproj/LocalizedAppboyUIString.strings hash A4gTpmd3RAdTOb5x7jO211+TLu8= optional zh-TW.lproj/LocalizedAppboyUIString.strings hash A4gTpmd3RAdTOb5x7jO211+TLu8= optional zh.lproj/LocalizedAppboyUIString.strings hash 6FRvJc2sxWJf+sECiTmCMl2VFOQ= optional files2 PrivacyInfo.xcprivacy hash 35537WDlOfHuEOldTrUa7OmzNwQ= hash2 vz81m6KH2HKZHoLSODBrzGNp/+lfzoLblqWy7rtuvpI= ZipArchive_LICENSE.txt hash JbmSdiuxe/Zs/esUY7pUvV4byF4= hash2 j7CoF6NtgM8sSqLkZ/syPjE4TgZL0oOqy+1tPp5ip/M= appboy-spm-cleanup.sh hash O3UfCL3lKKMB/+splkNXf6zYbbU= hash2 /l959MIHRRf5Hali4ZYnnwYbGGrbAruO2CSKS1WrggE= ar.lproj/LocalizedAppboyUIString.strings hash ikkSl37t+7dVXcdDSsplItVo5v4= hash2 Tasxj0n6Fu6eabJu/tAeixHF2EN9H9R3yEZ/dauIt7I= optional cs.lproj/LocalizedAppboyUIString.strings hash HpBIYzzQP2b/gz2ASERTvoi95m4= hash2 tnoN1JgdcOm+Gbs0kMTM0b1IS3cP+cWhB2WHpe7oOI8= optional da.lproj/LocalizedAppboyUIString.strings hash sg0g9Tgrlg3sJjwTl+G3PC8FAJg= hash2 xhUG+25LUOipRTwGtAmWNwyKMtXS6fUJRBZJ4Pc2oH8= optional de.lproj/LocalizedAppboyUIString.strings hash IoqO+svUlTLorH1d321C+DaCvLU= hash2 6y6k6zh0MxVioySbYA2ZWdiStPv22ikISkc9DDegkk8= optional en.lproj/LocalizedAppboyUIString.strings hash CcyvcSMMNgJPpJScduQ1DvYCPqc= hash2 NN+BQMMtBdLA+Yax5OiWpOamEQo1EhH/RAcqjMe7h+s= optional es-419.lproj/LocalizedAppboyUIString.strings hash LmBpUiGVEU/srcwSdgMN2xHVnsc= hash2 Fvh73dDedMMiSObM13PBR9iNZmE+izl7CXQ33O8edLc= optional es-MX.lproj/LocalizedAppboyUIString.strings hash LmBpUiGVEU/srcwSdgMN2xHVnsc= hash2 Fvh73dDedMMiSObM13PBR9iNZmE+izl7CXQ33O8edLc= optional es.lproj/LocalizedAppboyUIString.strings hash iAm4QTEmMOfm3uJbSU2CjYASqg4= hash2 oRsLMkaRaSzmu9MrHtp1Kj/fmQNsokuRc3b7gU+RIUw= optional et.lproj/LocalizedAppboyUIString.strings hash YRS0Fh60D+5z7QyzbRifH6lS5m0= hash2 IFkIJG3633h4s8vuCb/cNLLBoRTLm5TMYbriDREkRCY= optional fi.lproj/LocalizedAppboyUIString.strings hash ElciMeWd7+DdrGsUYIV1BCc9zts= hash2 sTMTM41FNd3QOc6PRgzFqpzKdHVf0Yav+heyxVGXozM= optional fil.lproj/LocalizedAppboyUIString.strings hash AwAsH+O3Oum5GBbG9U/UzAHs+Uo= hash2 4WSrZSSXSF0VkX/hZI9Jc3AeYK3oKs5m/K6mO1PyURE= optional fr.lproj/LocalizedAppboyUIString.strings hash 4MoSFYmXS1rkntVmD8Bdc7Hn5pw= hash2 sZwtzjFxqC4U1eGSwpPMtdSLxaT4TNCzMysev2RNW0k= optional he.lproj/LocalizedAppboyUIString.strings hash Rp7mkbk7PXmWefbaSE8FDXGXles= hash2 v2uh+X/qP+u6S9ZoY9ppeoXnfZmoTpBJPKSqzk7at5g= optional hi.lproj/LocalizedAppboyUIString.strings hash O8MWNHc3Um4NcsIrID5Vk4dd54s= hash2 OnU/X/MkvxSlf+OGy3RDF4vcdVyTtSog2KWRtC5oPYU= optional id.lproj/LocalizedAppboyUIString.strings hash 49qpfq7iRtfxxUbVInBDeKNjz7E= hash2 deYxzwkoBzdMrkWGd+f+VgSnx/s33CkitB+HZ0NZGO4= optional it.lproj/LocalizedAppboyUIString.strings hash laa/58WCKhmdGMlVjsTlBYUopG4= hash2 H69+kWo9xhAO7omzXIUzwgZh//sDyr0Gx0XXSs6FNoE= optional ja.lproj/LocalizedAppboyUIString.strings hash 3wDAz31yuk5UdZyTMdeUKUbgbeU= hash2 TFsM9+s7alWiWgpE6OyR2ZQteKpirSJ2ZEvwj/hnGxA= optional km.lproj/LocalizedAppboyUIString.strings hash PAlPrqyz9/Dr9FoyARSmLUUSNlY= hash2 azehywKf6sG34ERf6Wh5jwCJGs3zWQc+Qb9s/ML6Wtk= optional ko.lproj/LocalizedAppboyUIString.strings hash itXcFANBAU96clWcX1rA7p30JnE= hash2 Wkkbb8+TIExBHBF6tuLXThb3sR4dwrC3+4YlR2q2PAg= optional lo.lproj/LocalizedAppboyUIString.strings hash 9ummy0p0yIJpe1mMX7Xl03P0H/w= hash2 kZi+9bet5HA/jbyjGxu64lcw8hH0ZWv55g3SuTmuB8I= optional ms.lproj/LocalizedAppboyUIString.strings hash Gv7vjcrr6syw+oqHQ43wPMXrgNk= hash2 5Hr2LuS7C9LAEL/5/p+YDpe6+TwEwk63WMYaHUY8OI4= optional my.lproj/LocalizedAppboyUIString.strings hash 9yuPTyT4nWP0qa0okY7pGmudiV0= hash2 OGLTjMs+TI9BMp02BEY7Y9L/EwjJz+4xSIGnPnJfHBI= optional nb.lproj/LocalizedAppboyUIString.strings hash l80bnfoSc9rlRA4bXKJ0rwRC0OM= hash2 EeJLYfG14UVjDjg44lseegKl97KcnPX7h9SPGzeC7b0= optional nl.lproj/LocalizedAppboyUIString.strings hash /h2VZ4vvhTv86/q3pb2e1Egu/Sw= hash2 IbQbBgC+JoU+yAky0EtrojOVeeoczWsKXZbbqwcAsOo= optional pl.lproj/LocalizedAppboyUIString.strings hash BsEGyLvt3DQkwFvz66oYe7ua/Po= hash2 2OY0P3AYZpW/A6i3kAY3G1yJa7IV6Rt03pfWUza76Gs= optional pt-PT.lproj/LocalizedAppboyUIString.strings hash 4TjlnSbVdiRFyVbE2qBiMd+CyOQ= hash2 Z4Z/vNNS2oK6+pU5Fw2SlIto4qnFc4BNSysbg+4dKtg= optional pt.lproj/LocalizedAppboyUIString.strings hash zqP4s5SMAvHJJIetNS1R6ZE4O54= hash2 vRJuR2MouqsYKCHj7G/CcW37NOzKw/Z3SFVG/WQEZeY= optional ru.lproj/LocalizedAppboyUIString.strings hash STrDNbH4mbH8IiQ0hn8Vek5WgLQ= hash2 gHPNWQd/dis0H7ZLB4Pv8Buf+Asa/NmYiWVTyFRAtNk= optional sv.lproj/LocalizedAppboyUIString.strings hash mAlAT4rTioVuM3DYq5gih2kfI9o= hash2 lHcFD3AK/emy+aL3h6W3x8N0ToKNJroct782JbXL9UE= optional th.lproj/LocalizedAppboyUIString.strings hash DeAcN0mnPKHFwDbYuGg+sr5xk+0= hash2 i0DEOQlExvgorEkQ5lbY9KFETyTSfX0H+b05K0K0HY4= optional uk.lproj/LocalizedAppboyUIString.strings hash Tc4MJlZRNrNeCkLxxfRMq9EFW68= hash2 nl9a70ZKrLi8yHFhksJgNIMv8lHI/cEE09LQf9+IaAc= optional vi.lproj/LocalizedAppboyUIString.strings hash WnpBLIVxIeYHM1hAYdI5jgR/Y3o= hash2 PeGoDxEZIq6z/SuI09D8igvTMnJOPTkuSwgXG4mgxxM= optional zh-HK.lproj/LocalizedAppboyUIString.strings hash A4gTpmd3RAdTOb5x7jO211+TLu8= hash2 SFa2j6R9YoRZJ5/2EOUw3d2Xr7vh8iAHrNCjamSip3g= optional zh-Hans.lproj/LocalizedAppboyUIString.strings hash 6FRvJc2sxWJf+sECiTmCMl2VFOQ= hash2 fn83ZQteQmX1XgcF3J5cCPycXHscx1EIqBhU8RGqs5M= optional zh-Hant.lproj/LocalizedAppboyUIString.strings hash A4gTpmd3RAdTOb5x7jO211+TLu8= hash2 SFa2j6R9YoRZJ5/2EOUw3d2Xr7vh8iAHrNCjamSip3g= optional zh-TW.lproj/LocalizedAppboyUIString.strings hash A4gTpmd3RAdTOb5x7jO211+TLu8= hash2 SFa2j6R9YoRZJ5/2EOUw3d2Xr7vh8iAHrNCjamSip3g= optional zh.lproj/LocalizedAppboyUIString.strings hash 6FRvJc2sxWJf+sECiTmCMl2VFOQ= hash2 fn83ZQteQmX1XgcF3J5cCPycXHscx1EIqBhU8RGqs5M= optional rules ^.* ^.*\.lproj/ optional weight 1000 ^.*\.lproj/locversion.plist$ omit weight 1100 ^Base\.lproj/ weight 1010 ^version.plist$ rules2 .*\.dSYM($|/) weight 11 ^(.*/)?\.DS_Store$ omit weight 2000 ^.* ^.*\.lproj/ optional weight 1000 ^.*\.lproj/locversion.plist$ omit weight 1100 ^Base\.lproj/ weight 1010 ^Info\.plist$ omit weight 20 ^PkgInfo$ omit weight 20 ^embedded\.provisionprofile$ weight 20 ^version\.plist$ weight 20 ================================================ FILE: AppboyKit/Appboy.bundle/appboy-spm-cleanup.sh ================================================ #! /bin/sh # AppboyKitLibrary find "${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}" -name libAppboyKitLibrary.a -follow -exec rm {} \; # AppboyPushStory find "${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}" -name "AppboyPushStory.framework" -follow -exec rm -r {} \; ================================================ FILE: AppboyKit/include/ABKAttributionData.h ================================================ #import /* * Braze Public API: ABKAttributionData */ NS_ASSUME_NONNULL_BEGIN @interface ABKAttributionData : NSObject /*! * @param network The attribution network * @param campaign The attribution campaign * @param adGroup The attribution adGroup * @param creative The attribution creative * * @discussion: Creates an ABKAttributionData object to send to Braze servers. */ - (instancetype)initWithNetwork:(nullable NSString *)network campaign:(nullable NSString *)campaign adGroup:(nullable NSString *)adGroup creative:(nullable NSString *)creative; @property (nonatomic, readonly, nullable) NSString *network; @property (nonatomic, readonly, nullable) NSString *campaign; @property (nonatomic, readonly, nullable) NSString *adGroup; @property (nonatomic, readonly, nullable) NSString *creative; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKBannerCard.h ================================================ #import "ABKCard.h" /* * Braze Public API: ABKBannerCard */ NS_ASSUME_NONNULL_BEGIN @interface ABKBannerCard : ABKCard /* * This property is the URL of the card's image. */ @property (copy) NSString *image; /* * The link text for the property url, like @"blog.appboy.com". It can be displayed on the card's * UI to indicate the action/direction of clicking on the card. */ @property (copy, nullable) NSString *domain; /* * This property is the aspect ratio of the card's image. It is meant to serve as a hint before * image loading completes. Note that the property may not be supplied in certain circumstances. */ @property float imageAspectRatio; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKBannerContentCard.h ================================================ #import "ABKContentCard.h" @interface ABKBannerContentCard : ABKContentCard /* * The URL of the card's image. */ @property (copy) NSString *image; /* * This property is the aspect ratio of the card's image. It is meant to serve as a hint before * image loading completes. Note that the property may not be supplied in certain circumstances. */ @property float imageAspectRatio; @end ================================================ FILE: AppboyKit/include/ABKCaptionedImageCard.h ================================================ #import "ABKCard.h" /* * Braze Public API: ABKCaptionedImageCard */ NS_ASSUME_NONNULL_BEGIN @interface ABKCaptionedImageCard : ABKCard /* * This property is the URL of the card's image. */ @property (copy) NSString *image; /* * This property is the aspect ratio of the card's image. It is meant to serve as a hint before * image loading completes. Note that the property may not be supplied in certain circumstances. */ @property float imageAspectRatio; /* * The title text for the card. */ @property (copy) NSString *title; /* * The description text for the card. */ @property (copy) NSString *cardDescription; /* * The link text for the property url, like @"blog.appboy.com". It can be displayed on the card's * UI to indicate the action/direction of clicking on the card. */ @property (copy, nullable) NSString *domain; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKCaptionedImageContentCard.h ================================================ #import "ABKContentCard.h" NS_ASSUME_NONNULL_BEGIN @interface ABKCaptionedImageContentCard : ABKContentCard /* * The URL of the card's image. */ @property (copy) NSString *image; /* * This property is the aspect ratio of the card's image. It is meant to serve as a hint before * image loading completes. Note that the property may not be supplied in certain circumstances. */ @property float imageAspectRatio; /* * The title text for the card. */ @property (copy) NSString *title; /* * The description text for the card. */ @property (copy) NSString *cardDescription; /* * The link text for the property url, like @"blog.appboy.com". It can be displayed on the card's * UI to indicate the action/direction of clicking on the card. */ @property (copy, nullable) NSString *domain; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKCard.h ================================================ #import #import "ABKFeedController.h" /* * Braze Public API: ABKCard */ NS_ASSUME_NONNULL_BEGIN @interface ABKCard : NSObject /* * Card's ID. */ @property (readonly) NSString *idString; /* * This property reflects if the card is read or unread by the user. */ @property (nonatomic) BOOL viewed; /* * The property is the unix timestamp of the card's creation time from Braze dashboard. */ @property (nonatomic, readonly) double created; /* * The property is the unix timestamp of the card's latest update time from Braze dashboard. */ @property (nonatomic, readonly) double updated; /* * The categories assigned to the card. */ @property ABKCardCategory categories; /* * The property is the unix timestamp of the card's expiration time. When the value is less than 0, it means the card * doesn't an expire date. */ @property (readonly) double expiresAt; /*! * This property carries extra data in the form of an NSDictionary which can be sent down via the Braze Dashboard. * You may want to design and implement a custom handler to access this data depending on your use case. */ @property (strong, nullable) NSDictionary *extras; //Optional: /* * The URL string that will be opened after the card is clicked on. */ @property (copy, nullable) NSString *urlString; /*! * When the card's urlString is not nil, if the property is set to YES, the URL will be opened in a modal WKWebView * inside the app. If this property is set to NO, the URL will be opened by the OS and web URLs will be opened in * an external web browser app. * * This property defaults to NO. */ @property BOOL openUrlInWebView; /* * @param cardDictionary The dictionary for card deserialization. * * Deserializes the dictionary to a card for use by wrappers such as Braze's Unity SDK for iOS. * When the deserialization isn't successful, this method returns nil; otherwise, it returns the deserialized card. */ + (nullable ABKCard *)deserializeCardFromDictionary:(nullable NSDictionary *)cardDictionary; /* * Serializes the card to binary data for use by wrappers such as Braze's Unity SDK for iOS. */ - (nullable NSData *)serializeToData; /* * Manually log an impression to Braze for the card. * This should only be used for custom news feed view controller. ABKFeedViewController already has card impression logging. */ - (void)logCardImpression; /* * Manually log a click to Braze for the card. * This should only be used for custom news feed view controller. ABKFeedViewController already has card click logging. * The SDK will only log a card click when the card has the url property with a valid url. */ - (void)logCardClicked; - (BOOL)hasSameId:(ABKCard *)card; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKClassicCard.h ================================================ #import "ABKCard.h" /* * Braze Public API: ABKClassicCard */ NS_ASSUME_NONNULL_BEGIN @interface ABKClassicCard : ABKCard /* * This property is the URL of the card's image. */ @property (copy, nullable) NSString *image; /* * The description text for the card. */ @property (copy) NSString *cardDescription; /* * The news title text for the card. */ @property (copy, nullable) NSString *title; /* * The link text for the property url, like @"blog.appboy.com". It can be displayed on the card's * UI to indicate the action/direction of clicking on the card. */ @property (copy, nullable) NSString *domain; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKClassicContentCard.h ================================================ #import "ABKContentCard.h" NS_ASSUME_NONNULL_BEGIN @interface ABKClassicContentCard : ABKContentCard /* * The URL of the card's image. */ @property (copy, nullable) NSString *image; /* * The news title text for the card. */ @property (copy) NSString *title; /* * The description text for the card. */ @property (copy) NSString *cardDescription; /* * The link text for the property url, like @"blog.appboy.com". It can be displayed on the card's * UI to indicate the action/direction of clicking on the card. */ @property (copy, nullable) NSString *domain; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKContentCard.h ================================================ #import /* * Braze Public API: ABKContentCard */ NS_ASSUME_NONNULL_BEGIN @interface ABKContentCard : NSObject /*! * Card's ID. */ @property (readonly) NSString *idString; /*! * This property reflects if the card is read or unread by the user. */ @property (nonatomic) BOOL viewed; /*! * The property is the unix timestamp of the card's creation time from Braze dashboard. */ @property (nonatomic, readonly) double created; /*! * The property is the unix timestamp of the card's expiration time. When the value is less than 0, it means the card * doesn't an expire date. */ @property (readonly) double expiresAt; /*! * This property reflects if the card can be dismissed by the user. */ @property (nonatomic) BOOL dismissible; /*! * This property reflects if the card has been pinned by the user. */ @property (nonatomic) BOOL pinned; /*! * This property reflects if the card has been dimissed. */ @property (nonatomic) BOOL dismissed; /*! * This property reflects if the card has been clicked. */ @property (nonatomic) BOOL clicked; /*! * This property carries extra data in the form of an NSDictionary which can be sent down via the Braze Dashboard. * You may want to design and implement a custom handler to access this data depending on your use case. */ @property (strong, nullable) NSDictionary *extras; /*! * This property is set to YES if the instance represents a test content card */ @property (nonatomic, readonly) BOOL isTest; /*! * The URL string that will be opened after the card is clicked on. */ @property (copy, nullable) NSString *urlString; /*! * When the card's urlString is not nil, if the property is set to YES, the URL will be opened in a modal WKWebView * inside the app. If this property is set to NO, the URL will be opened by the OS and web URLs will be opened in * an external web browser app. * * This property defaults to NO. */ @property BOOL openUrlInWebView; /*! * @param cardDictionary The dictionary for card deserialization. * * Deserializes the dictionary to a card for use by wrappers such as Braze's Unity SDK for iOS. * When the deserialization isn't successful, this method returns nil; otherwise, it returns the deserialized card. */ + (nullable ABKContentCard *)deserializeCardFromDictionary:(nullable NSDictionary *)cardDictionary; /*! * Serializes the card to binary data for use by wrappers such as Braze's Unity SDK for iOS. */ - (nullable NSData *)serializeToData; /*! * Manually log an impression to Braze for the card. * This should only be used for custom content card view controllers. */ - (void)logContentCardImpression; /*! * Manually log a click to Braze for the card. * This should only be used for custom contentcard view controllers. */ - (void)logContentCardClicked; /*! * Manually dismiss a card. * Sets the card's `dismissed` property to YES and logs the dismissal to Braze. * Only has effect if the card is dismissible and if the `dismissed` property is currently set to NO. */ - (void)logContentCardDismissed; - (BOOL)isControlCard; - (BOOL)hasSameId:(ABKContentCard *)card; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKContentCardsController.h ================================================ #import /* ------------------------------------------------------------------------------------------------------ * Notifications */ /*! * When Content Cards are updated, Braze will post a notification through the NSNotificationCenter. * The name of the notification is the string constant referred to by ABKContentCardsProcessedNotification. The * userInfo dictionary associated with the notification will has one object, with key the same string * as ABKContentCardsProcessedIsSuccessfulKey, to indicate whether the update is successful or not. * * To listen for this notification, you would register an object as an observer of the notification * using something like: * *
 *   [[NSNotificationCenter defaultCenter] addObserver:self
 *                                            selector:@selector(contentCardsUpdatedNotificationReceived:)
 *                                                name:ABKContentCardsProcessedNotification
 *                                              object:nil];
 * 
* * where "contentCardsUpdatedNotificationReceived:" is your callback method for handling the notification: * *
 *   - (void)contentCardsUpdatedNotificationReceived:(NSNotification *)notification {
 *     BOOL updateIsSuccessful = [notification.userInfo[ABKContentCardsProcessedIsSuccessfulKey] boolValue];
 *     < Check if update was successful and do something in response to the notification >
 *   }
 * 
*/ NS_ASSUME_NONNULL_BEGIN extern NSString *const ABKContentCardsProcessedNotification; extern NSString *const ABKContentCardsProcessedIsSuccessfulKey; /* * Braze Public API: ABKContentCardsController */ @interface ABKContentCardsController : NSObject /*! * The latest content cards that are saved in memory and disk. */ @property (readonly, getter=getContentCards) NSArray *contentCards; /*! * The NSDate object that indicates the last time the contentCards property was updated from Braze server. */ @property (readonly, nullable) NSDate *lastUpdate; /*! * Returns the count of unviewed cards, excluding control cards. * A "view" happens when a card becomes visible in the Content Cards view. This differentiates * between cards which are off-screen in the scrolling view, and those which * are on-screen; when a card scrolls onto the screen, it's counted as viewed. * * Cards are counted as viewed only once -- if a card scrolls off the screen and * back on, it's not re-counted. * * Cards are counted only once even if they appear in multiple Content Cards views or across multiple devices. */ - (NSInteger)unviewedContentCardCount; /*! * Returns the count of available cards, including control cards. * Cards are counted only once even if they appear in multiple Content Cards views. */ - (NSInteger)contentCardCount; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKFacebookUser.h ================================================ #import #import "ABKUser.h" NS_ASSUME_NONNULL_BEGIN extern NSInteger const DefaultNumberOfFriends; /* * Braze Public API: ABKFacebookUser */ @interface ABKFacebookUser : NSObject /*! * @param facebookUserDictionary The dictionary returned from facebook with facebook graph api endpoint "/me". Please * refer to https://developers.facebook.com/docs/graph-api/reference/v4.0/user for more information. * @param numberOfFriends The length of the friends array from facebook. You can get the array from the dictionary returned * from facebook with facebook graph api endpoint "/me/friends", under the key "data". Please refer to * https://developers.facebook.com/docs/graph-api/reference/v4.0/user/friends for more information. * @param likes The array of user's facebook likes from facebook. You can get the array from the dictionary returned * from facebook with facebook graph api endpoint "/me/likes", under the key "data"; Please refer to * https://developers.facebook.com/docs/graph-api/reference/v4.0/user/likes for more information. * * @discussion: This method is to generate a ABKFacebookUser so you can pass the user's facebook account data to Braze. * After a ABKFacebookUser object is generated, you can check the value of properties but you cannot change it. * If you want to update the user's facebook data, you need to generate a new ABKFacebookUser instance and set it as * [Appboy sharedInstance].user.facebookUser. */ - (instancetype)initWithFacebookUserDictionary:(nullable NSDictionary *)facebookUserDictionary numberOfFriends:(NSInteger)numberOfFriends likes:(nullable NSArray *)likes; @property (readonly, nullable) NSDictionary *facebookUserDictionary; @property (readonly) NSInteger numberOfFriends; @property (readonly, nullable) NSArray *likes; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKFeedController.h ================================================ #import /* ------------------------------------------------------------------------------------------------------ * Notifications */ /*! * When the news feed is updated, Braze will post a notification through the NSNotificationCenter. * The name of the notification is the string constant referred to by ABKFeedUpdatedNotification. The * userInfo dictionary associated with the notification will has one object, with key the same string * as ABKFeedUpdatedIsSuccessfulKey, to indicate whether the update is successful or not. * * To listen for this notification, you would register an object as an observer of the notification * using something like: * *
 *   [[NSNotificationCenter defaultCenter] addObserver:self
 *                                            selector:@selector(feedUpdatedNotificationReceived:)
 *                                                name:ABKFeedUpdatedNotification
 *                                              object:nil];
 * 
* * where "feedUpdatedNotificationReceived:" is your callback method for handling the notification: * *
 *   - (void)feedUpdatedNotificationReceived:(NSNotification *)notification {
 *     BOOL updateIsSuccessful = [notification.userInfo[ABKFeedUpdatedIsSuccessfulKey] boolValue];
 *     < Do something in response to the notification >
 *   }
 * 
*/ NS_ASSUME_NONNULL_BEGIN extern NSString *const ABKFeedUpdatedNotification; extern NSString *const ABKFeedUpdatedIsSuccessfulKey; /* ------------------------------------------------------------------------------------------------------ * Enums */ /*! * Values representing the news feed cards' categories recognized by the SDK. */ typedef NS_OPTIONS(NSUInteger, ABKCardCategory) { ABKCardCategoryNoCategory = 1 << 0, ABKCardCategoryNews = 1 << 1, ABKCardCategoryAdvertising = 1 << 2, ABKCardCategoryAnnouncements = 1 << 3, ABKCardCategorySocial = 1 << 4, ABKCardCategoryAll = 1 << 0 | 1 << 1 | 1 << 2 | 1 << 3 | 1 << 4 }; /* * Braze Public API: ABKFeedController */ @interface ABKFeedController : NSObject /*! * The latest cards of the Braze News Feed saved in memory and disk. Right now the available card types are ABKBannerCard, * ABKCaptionedImageCard, ABKClassicCard and ABKTextAnnouncementCard. They are all subclasses * of ABKCard. */ @property (readonly, getter=getNewsFeedCards) NSArray *newsFeedCards; /*! * The NSDate object that indicates the last time the newsFeedCards property was updated from the Braze server. */ @property (readonly, nullable) NSDate *lastUpdate; /*! * This method returns the number of currently active cards which have not been viewed in the given categories. * A "view" happens when a card becomes visible in the feed view. This differentiates * between cards which are off-screen in the scrolling view, and those which * are on-screen; when a card scrolls onto the screen, it's counted as viewed. * * Cards are counted as viewed only once -- if a card scrolls off the screen and * back on, it's not re-counted. * * Cards are counted only once even if they appear in multiple feed views or across multiple devices. */ - (NSInteger)unreadCardCountForCategories:(ABKCardCategory)categories; /*! * This method returns the total number of currently active cards belongs to given categories. Cards are * counted only once even if they appear in multiple feed views. */ - (NSInteger)cardCountForCategories:(ABKCardCategory)categories; /*! * @param categories An ABKCardCategory indicating the categories that you want to get. You can pass more than one category * at one time by using "|" to separate categories like: ABKCardCategoryNews | ABKCardCategoryAnnouncements | ABKCardCategorySocial * @return An array of cards of the given categories. * * @discussion This method will find the cards of given categories and return them. * When the given categories don't exist in any card, this method will return an empty array. */ - (NSArray *)getCardsInCategories:(ABKCardCategory)categories; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKIDFADelegate.h ================================================ #import /* * Braze Public API: ABKAppboyIDFADelegate */ NS_ASSUME_NONNULL_BEGIN @protocol ABKIDFADelegate /*! * Asks the delegate to return a valid IDFA for the current user. * * Use this delegate to pass the IDFA to Braze. Braze does not collect IDFA automatically. * * @return The current users's IDFA UUID. */ - (NSString *)advertisingIdentifierString; /*! * Asks the delegate to return whether advertising tracking is enabled for the current user. * * Your delegate implementation should use ATTrackingManager on iOS 14+ and ASIdentifierManager on earlier iOS versions. * * An example implementation is available here: * https://github.com/Appboy/appboy-ios-sdk/blob/master/Example/Stopwatch/Sources/Utils/IDFADelegate.m * * @return YES if advertising tracking is enabled for iOS 14 and earlier or if AppTrackingTransparency (ATT) is authorized with iOS 14+, NO otherwise */ - (BOOL)isAdvertisingTrackingEnabledOrATTAuthorized; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKImageDelegate.h ================================================ #import NS_ASSUME_NONNULL_BEGIN /* * This delegate protocol gives the Braze iOS SDK access to the image framework. */ typedef NS_OPTIONS(NSUInteger, ABKImageOptions ) { ABKImageOptionsRetryFailed = 1 << 0, ABKImageOptionsLowPriority = 1 << 1, ABKImageOptionsCacheMemoryOnly = 1 << 2, ABKImageOptionsProgressiveDownload = 1 << 3, ABKImageOptionsRefreshCached = 1 << 4, ABKImageOptionsContinueInBackground = 1 << 5, ABKImageOptionsHandleCookies = 1 << 6, }; @protocol ABKImageDelegate - (void)setImageForView:(UIImageView *)imageView showActivityIndicator:(BOOL)showActivityIndicator withURL:(nullable NSURL *)imageURL imagePlaceHolder:(nullable UIImage *)placeHolder completed:(nullable void (^)(UIImage * _Nullable image, NSError * _Nullable error, NSInteger cacheType, NSURL * _Nullable imageURL))completion; - (void)loadImageWithURL:(nullable NSURL *)url options:(ABKImageOptions)options completed:(nullable void(^)(UIImage *image, NSData *data, NSError *error, NSInteger cacheType, BOOL finished, NSURL *imageURL))completion; - (void)diskImageExistsForURL:(nullable NSURL *)url completed:(nullable void (^)(BOOL isInCache))completion; - (nullable UIImage *)imageFromCacheForURL:(nullable NSURL *)url; /*! * @discussion Returns a class that is UIImageView or a subclass of UIImageView to allow the implementor to bring their own * implementation of animated image support. */ - (Class)imageViewClass; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKInAppMessage.h ================================================ #import #import @class ABKInAppMessageDarkTheme; /*! * The ABKInAppMessageClickActionType defines the action that will be performed when the in-app message is clicked. * * ABKInAppMessageDisplayNewsFeed - This is the default behavior. It will open a modal view of Braze news feed. * * ABKInAppMessageRedirectToURI - The in-app message will try to redirect to the uri defined by the uri property. Only when the uri * is an HTTP URL, a modal web view will be displayed. If the uri is a protocol uri, the in-app message will redirect to the * protocol uri. * * ABKInAppMessageNoneClickAction - The in-app message will do nothing but dismiss itself. */ typedef NS_ENUM(NSInteger, ABKInAppMessageClickActionType) { ABKInAppMessageDisplayNewsFeed, ABKInAppMessageRedirectToURI, ABKInAppMessageNoneClickAction }; /*! * The ABKInAppMessageDismissType defines how the in-app message can be dismissed. * * ABKInAppMessageDismissAutomatically - This is the default behavior for ABKInAppMessageSlideup. * It will dismiss after the length of time defined by the duration property. * ABKInAppMessageSlideup of this type can also be dismissed by swiping. * * ABKInAppMessageDismissManually - This is the default behavior for ABKInAppMessageImmersive. The * in-app message will stay on the screen indefinitely unless dismissed by swiping or a click on * the close button. */ typedef NS_ENUM(NSInteger, ABKInAppMessageDismissType) { ABKInAppMessageDismissAutomatically, ABKInAppMessageDismissManually }; /*! * The ABKInAppMessageOrientation defines preferred screen orientation for the in-app message. * * ABKInAppMessageOrientationAny - This is the default value for an in-app message's orientation. This * value allows the in-app message display in any orientation. * * ABKInAppMessageOrientationPortrait - This value will limit the in-app message to only display in * protrait and portrait upside down orientation. * * ABKInAppMessageOrientationLandscape - This value will limit the in-app message to only display in * landscape orientation, including landscape left and landscape right. */ typedef NS_ENUM(NSInteger, ABKInAppMessageOrientation) { ABKInAppMessageOrientationAny, ABKInAppMessageOrientationPortrait, ABKInAppMessageOrientationLandscape }; /*! * Default icon and in-app message button background colors. * These are used in the in-app message view controllers. */ static CGFloat const RedValueOfDefaultIconColorAndButtonBgColor = (CGFloat)0.0f; static CGFloat const GreenValueOfDefaultIconColorAndButtonBgColor = (CGFloat)(115.0f / 255.0f); static CGFloat const BlueValueOfDefaultIconColorAndButtonBgColor = (CGFloat)(213.0f / 255.0f); static CGFloat const AlphaValueOfDefaultIconColorAndButtonBgColor = (CGFloat)1.0f; /* * Braze Public API: ABKInAppMessage */ NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessage : NSObject /*! * This property defines the message displayed within the in-app message. */ @property (copy) NSString *message; /*! * This property carries extra data in the form of an NSDictionary which can be sent down via the Braze Dashboard. * You may want to design and implement a custom handler to access this data depending on your use-case. */ @property (strong, nullable) NSDictionary *extras; /*! * This property defines the number of seconds before the in-app message is automatically dismissed. */ @property (nonatomic) NSTimeInterval duration; /*! * This property defines the action that will be performed when the in-app message is clicked. * See the ABKInAppMessageClickActionType enum documentation above offers additional details. */ @property (readonly) ABKInAppMessageClickActionType inAppMessageClickActionType; /*! * When the in-app message's inAppMessageClickActionType is ABKInAppMessageRedirectToURI, clicking on the in-app message will redirect to the uri defined * in this property. * * This property can be a HTTP URI or a protocol URI. */ @property (readonly, copy, nullable) NSURL *uri; /*! * When the in-app message's inAppMessageClickActionType is ABKInAppMessageRedirectToURI, if the property is set to YES, * the URI will be opened in a modal WKWebView inside the app. If this property is set to NO, the URI will be opened by * the OS and web URIs will be opened in an external web browser app. * * This property defaults to YES on ABKInAppMessageHTML subclasses and NO on all other ABKInAppMessage subclasses. */ @property BOOL openUrlInWebView; /*! * inAppMessageDismissType defines the dismissal behavior of the in-app message. * See the above documentation for ABKInAppMessageDismissType for additional details. */ @property ABKInAppMessageDismissType inAppMessageDismissType; /*! * backgroundColor defines the background color of the in-app message. The default background color is black with 0.9 alpha for * ABKInAppMessageSlideup, and white with 1.0 alpha for ABKInAppMessageModal and ABKInAppMessageFull. */ @property (nonatomic, strong, nullable) UIColor *backgroundColor; /*! * textColor defines the message text color of the in-app message. The default text color is black. */ @property (nonatomic, strong, nullable) UIColor *textColor; /*! * icon the unicode string of the Font Awesome icon for this in-app message. * * You may add Font Awesome icons to in-app messages from the Braze dashboard. */ @property (nonatomic, copy, nullable) NSString *icon; /*! * iconColor defines the font color of icon property. * The default font color is white. */ @property (nonatomic, strong, nullable) UIColor *iconColor; /*! * iconBackgroundColor defines the background color of icon property. * * The default background color's RGB values are R:0 G:115 B:213. */ @property (nonatomic, strong, nullable) UIColor *iconBackgroundColor; /*! * This boolean determines if the in-app message will attempt to use dark theme colors, granted the device * is in dark mode and the fields are present in the response. * * @discussion The default of this value is YES but can be overriden in `beforeInAppMessageDisplayed:` * to ensure that the dark theme is disabled for any given in-app message. */ @property (nonatomic, assign) BOOL enableDarkTheme; /*! * Data model that contains all the dark theme color info for any visible views, including any buttons * that may be present. */ @property (nonatomic, strong, nullable) ABKInAppMessageDarkTheme *darkTheme; /*! * An optional UIUserInterfaceStyle that can be used to force dark or light mode. * * @discussion The default value will not override OS settings but can * be overriden in `beforeInAppMessageDisplayed:` * to ensure that the dark or light theme is used for any given in-app message. * This property is of type NSInteger to avoid any iOS version dependencies. */ @property (nonatomic) NSInteger overrideUserInterfaceStyle; /*! * imageURI defines the URI of the image icon on in-app message. * When there is a iconImage defined, the iconImage will be used and the value of property icon will * be ignored. */ @property (copy, nullable) NSURL *imageURI; /*! * imageContentMode defines the content mode of the image on in-app message. * For immersive in-app messages, the imageContentMode defines both the image icon and the graphic * image's content mode. * * The imageContentMode default values are: * Slideup: UIViewContentModeScaleAspectFit * Modal: UIViewContentModeScaleAspectFit * Full: UIViewContentModeScaleAspectFill */ @property UIViewContentMode imageContentMode; /*! * orientation defines the preferred screen orientation for the in-app message. * In-app messages will only display if the preferred orientation matches the current status bar * orientation. However, there is an important exception for iPads. For in-app messages that * have a preferred orientation and are being displayed on an iPad, the in-app message will appear * in the style of the preferred orientation regardless of actual screen orientation. */ @property ABKInAppMessageOrientation orientation; /*! * messageTextAlignment defines the preferred text alignment of the message label. * The default values are: * Slideup: NSTextAlignmentNatural * Modal: NSTextAlignmentCenter * Full: NSTextAlignmentCenter */ @property NSTextAlignment messageTextAlignment; /* * animateIn/animateOut define if the in-app message should be animated in/out on the screen when * displaying/dismissing. The default value is YES. */ @property BOOL animateIn; @property BOOL animateOut; /*! * isControl defines whether this in-app message is a control. Control in-app messages should not be displayed to users. */ @property BOOL isControl; /*! * If you're handling in-app messages completely on your own, you should still report * impressions and clicks on the in-app message back to Braze with these methods so that your campaign reporting features * still work in the dashboard. * * Note: Each in-app message can log at most one impression and at most one click. */ - (void)logInAppMessageImpression; - (void)logInAppMessageClicked; /*! * This method will set the inAppMessageClickActionType property. * * When clickActionType is ABKInAppMessageRedirectToURI, the parameter uri cannot be nil. When clickActionType is * ABKInAppMessageDisplayNewsFeed or ABKInAppMessageNoneClickAction, the parameter uri will be ignored, and property uri * will be set to nil. */ - (void)setInAppMessageClickAction:(ABKInAppMessageClickActionType)clickActionType withURI:(nullable NSURL *)uri; /*! * Serializes the in-app message to binary data for use by wrappers such as Braze's Unity SDK for iOS. */ - (nullable NSData *)serializeToData; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKInAppMessageButton.h ================================================ #import "ABKInAppMessage.h" /* * Braze Public API: ABKInAppMessageButton */ NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessageButton : NSObject /*! * This property defines the button title text in UIControlStateNormal. Setting this property will also change the button * title text. */ @property (copy, nullable) NSString *buttonText; /*! * This property defines the button's background color. */ @property (strong, nullable) UIColor *buttonBackgroundColor; /*! * This property defines the button's border color. * If this property is not sent from the server, the background color is used. */ @property (strong, nullable) UIColor *buttonBorderColor; /*! * This property defines the button's title color in UIControlStateNormal. Setting this property will also change the * button title color. */ @property (strong, nullable) UIColor *buttonTextColor; /*! * This property defines the button title font in UIControlStateNormal. Please set this property before the in-app message * is displayed, or the displayed in-app message will not apply the font. */ @property (copy, nullable) UIFont *buttonTextFont; /*! * This property defines the action that will be performed when the button is clicked. * See the ABKInAppMessageClickActionType enum documentation in ABKInAppMessage.h offers additional details. */ @property (readonly) ABKInAppMessageClickActionType buttonClickActionType; /*! * When the button's buttonClickActionType is ABKInAppMessageRedirectToURI, clicking on the button will redirect to the uri * defined in this property. * * This property can be a HTTP URI or a protocol URI. */ @property (readonly, copy, nullable) NSURL *buttonClickedURI; /*! * When the button's buttonClickActionType is ABKInAppMessageRedirectToURI, if the property is set to YES, * the URI will be opened in a modal WKWebView inside the app. If this property is set to NO, the URI will be opened by * the OS and web URIs will be opened in an external web browser app. * * This property defaults to NO. */ @property BOOL buttonOpenUrlInWebView; /*! * This property defines the button's ID. Button's ID is used to track user's clicking action and used for corresponding * data analytics. */ @property (readonly) NSInteger buttonID; /*! * This method will set the buttonClickActionType property. * * When clickActionType is ABKInAppMessageRedirectToURI, the parameter uri cannot be nil, and the value will be passed to * buttonClickedURI. When clickActionType is ABKInAppMessageDisplayNewsFeed or ABKInAppMessageNoneClickAction, the * parameter uri will be ignored, and property uri will be set to nil. */ - (void)setButtonClickAction:(ABKInAppMessageClickActionType)clickActionType withURI:(nullable NSURL *)uri; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKInAppMessageControl.h ================================================ #import "ABKInAppMessage.h" /* * Braze Public API: ABKInAppMessageControl */ @interface ABKInAppMessageControl : ABKInAppMessage @end ================================================ FILE: AppboyKit/include/ABKInAppMessageController.h ================================================ #import #import "ABKInAppMessage.h" #import "ABKInAppMessageControllerDelegate.h" #import "ABKInAppMessageUIControlling.h" /*! Note: This class is not thread safe and all class methods should be called from the main thread.*/ /* * Braze Public API: ABKInAppMessageController */ NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessageController : NSObject /*! * Setting the delegate allows your app to control how, when, and if in-app messages are displayed. * Your app can set the delegate to override the default behavior of the ABKInAppMessageController. See * ABKInAppMessageControllerDelegate.h for more information. */ @property (weak, nonatomic, nullable) id delegate; /*! * If you have implemented the In-App Message subspec, you can use the ABKInAppMessageUIController to control * in-app message behavior. See ABKInAppMessageUIController for more information. */ @property (strong, nonatomic, nullable) id inAppMessageUIController; /*! * This boolean determines if modal in-app messages will be dismissed when the user taps outside of the * in-app message. * * @discussion The default of this value is NO but can be overriden by setting the value of ABKEnableDismissModalOnOutsideTapKey in * appboyOptions or in the Braze dictionary in your Info.plist file. */ @property BOOL enableDismissModalOnOutsideTap; /*! * @param delegate The in-app message delegate that implements the ABKInAppMessageControllerDelegate methods. If the delegate is * nil, it acts as one which always returns ABKDisplayInAppMessageNow and doesn't implement all other delegate methods. * * @discussion This method grabs the next in-app message from the in-app message stack, if there is one, and displays it with * the provided delegate. The delegate must return a ABKInAppMessageDisplayChoice that defines how the in-app message will be * handled. Please refer to the ABKInAppMessageDisplayChoice enum documentation for more detailed information. * * If there are no in-app messages available this returns immediately having taken no action. */ - (void)displayNextInAppMessageWithDelegate:(nullable id)delegate __deprecated_msg("Please use 'displayNextInAppMessage' instead."); /*! * Displays the next in-app message from the in-app message stack. * * This method pops the next in-app message from the in-app message stack and tries to displays it. * When defined, the current delegate methods are executed to respect any custom behavior. */ - (void)displayNextInAppMessage; /*! * @return The number of in-app messages that are locally waiting to be displayed. * * @discussion Use this method to check how many in-app messages are waiting to be displayed and call * displayNextInAppMessageWithDelegate: at to display it. If an in-app message is currently being displayed, it will not be included * in the count. * * Note: Returning ABKDisplayInAppMessageLater in the beforeInAppMessageDisplayed: delegate method will put the in-app message back onto * the stack and this will be reflected in inAppMessagesRemainingOnStack. */ - (NSInteger)inAppMessagesRemainingOnStack; /*! * @discussion This method allows you to request display of an in-app message. It adds the in-app message object to the top of the in-app message stack * and tries to display it immediately. * * If you add an ABKInAppMessage instance that you received through a Braze delegate method - i.e. one that is associated with a campaign or Canvas, * then impression and click analytics will work automatically. If you add an ABKInAppMessage instance that you instantiated yourself programmatically * (uncommon), then analytics will not be available. * * @param newInAppMessage the in-app message to add. */ - (void)addInAppMessage:(ABKInAppMessage *)newInAppMessage; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKInAppMessageControllerDelegate.h ================================================ #import #import "ABKInAppMessage.h" NS_ASSUME_NONNULL_BEGIN /*! * Possible values for in-app message handling after a in-app message is offered to an ABKInAppMessageControllerDelegate * ABKDisplayInAppMessageNow - The in-app message will be displayed immediately. * ABKDisplayInAppMessageLater - The in-app message will be not be displayed and will be placed back onto the top of the stack. * ABKDiscardInAppMessage - The in-app message will be discarded and will not be displayed. * ABKReenqueueInAppMessage - The in-app message will not be displayed and all trigger times and re-eligibility will rollback as if the trigger never occurred. * * The following conditions can cause a in-app message to be offered to the delegate defined by the delegate property on * [Appboy sharedInstance].inAppMessageController: * - A in-app message is received from the Braze server. * - A in-app message is waiting to display when an UIApplicationDidBecomeActiveNotification event occurs. * - A in-app message is added by ABKInAppMessageController method addInAppMessage:. * * You can choose to manually display any in-app messages that are waiting locally to be displayed by calling: * [[Appboy sharedInstance].inAppMessageController displayNextInAppMessage]. */ typedef NS_ENUM(NSInteger, ABKInAppMessageDisplayChoice) { ABKDisplayInAppMessageNow, ABKDisplayInAppMessageLater, ABKDiscardInAppMessage, ABKReenqueueInAppMessage, }; typedef NS_ENUM(NSInteger, ABKTriggerEventType) { ABKTriggerEventTypeSessionStart, ABKTriggerEventTypeCustomEvent, ABKTriggerEventTypePurchase, ABKTriggerEventTypeOther }; /*! * The in-app message delegate allows you to control the display of the Braze in-app message. For more detailed * information on in-app message behavior, including when and how the delegate is used, see the documentation for the * ABKInAppMessageDisplayChoice enum above for more detailed information. * * This delegate is for those who are using the Core subspec and not integrating the In-App Message subspec. If * you are using the In-App Message subspec, please use ABKInAppMessageUIDelegate. */ /*! * Braze Public API: ABKInAppMessageControllerDelegate */ @protocol ABKInAppMessageControllerDelegate @optional /*! * @param inAppMessage The in-app message object being offered to the delegate method. * @return ABKInAppMessageDisplayChoice The in-app message display choice. For details refer to the documentation regarding the ENUM ABKInAppMessageDisplayChoice * above. * * This delegate method defines whether the in-app message will be displayed now, displayed later, or discarded. * * If there are situations where you would not want the in-app message to appear (such as during a full screen * game or on a loading screen), you can use this delegate to delay or discard pending in-app message messages. */ - (ABKInAppMessageDisplayChoice)beforeInAppMessageDisplayed:(ABKInAppMessage *)inAppMessage; /*! * @param inAppMessage The control in-app message object being offered to the delegate method. * @return ABKInAppMessageDisplayChoice The control in-app message impression logging choice. * For details refer to the documentation regarding the ENUM ABKInAppMessageDisplayChoice above. * Logging a control message impression is an equivalent of displaying the message, except that no actual display occurs. * * This delegate method defines the timing of when the control in-app message impression event should be logged: now, later, discarded, or re-enqueued. * Logging a control message impression is an equivalent of displaying the message, except that no actual display occurs. * * If there are situations where you would not want the control in-app message impression to be logged, you can use this delegate to delay * or discard it. */ - (ABKInAppMessageDisplayChoice)beforeControlMessageImpressionLogged:(ABKInAppMessage *)inAppMessage; /*! * Executed when no trigger matches the Braze event. * * @param eventType The type of event that failed to match the user's triggers. * @param name The event name of a custom event, the product identifier for a purchase * event, or `nil` for a session start event. */ - (void)noMatchingTriggerForEvent:(ABKTriggerEventType)eventType name:(nullable NSString *)name; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKInAppMessageDarkButtonTheme.h ================================================ #import #import NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessageDarkButtonTheme : NSObject /*! * Dark theme of the button's background color. */ @property (strong) UIColor *buttonBackgroundColor; /*! * Dark theme of the button's border color. */ @property (strong) UIColor *buttonBorderColor; /*! * Dark theme of the button's text color. */ @property (strong) UIColor *buttonTextColor; /*! * Creates a model containing the dark theme colors for buttons by parsing the dictionary `darkButtonFields` */ - (instancetype)initWithFields:(NSDictionary *)darkButtonFields; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKInAppMessageDarkTheme.h ================================================ #import #import @class ABKInAppMessageButton; @class ABKInAppMessageDarkButtonTheme; NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessageDarkTheme : NSObject /* Properties of all ABKInAppMessages */ @property (nonatomic, strong, nullable) UIColor *backgroundColor; @property (nonatomic, strong, nullable) UIColor *textColor; @property (nonatomic, strong, nullable) UIColor *iconColor; @property (nonatomic, strong, nullable) UIColor *iconBackgroundColor; /* ABKInAppMessageImmersive only */ @property (nonatomic, strong, nullable) UIColor *headerTextColor; @property (nonatomic, strong, nullable) UIColor *closeButtonColor; @property (nonatomic, strong, nullable) UIColor *frameColor; /*! * An array of all the button color properties, in the same order as the buttons object in ABKInAppImmersive */ @property (nonatomic, strong, nullable) NSArray *buttons; /*! * Data model storing all the Dark Theme values passed down from the server for an in-app message. * This only gets initalized if the campaign is set up to support Dark Theme and has the fields populated. */ - (instancetype)initWithFields:(NSDictionary *)darkThemeFields; /*! * Returns the dark color variant given a valid key. If the key isn't found, returns nil. */ - (UIColor *)getColorForKey:(NSString *)key; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKInAppMessageFull.h ================================================ #import "ABKInAppMessageImmersive.h" /* * Braze Public API: ABKInAppMessageFull */ NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessageFull : ABKInAppMessageImmersive @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKInAppMessageHTML.h ================================================ #import #import "ABKInAppMessageHTMLBase.h" /* * Braze Public API: ABKInAppMessageHTML */ NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessageHTML : ABKInAppMessageHTMLBase /*! * This property indicates whether the content was built by our platform. */ @property (nonatomic) BOOL trusted; /*! * This property is an array of asset URLs that are used when generating the HTML. */ @property (strong, nonatomic, nullable) NSArray *assetUrls; /*! * This property is a dictionary of other structured data that can be included with the in-app message. */ @property (strong, nonatomic, nullable) NSDictionary *messageFields; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKInAppMessageHTMLBase.h ================================================ #import #import "ABKInAppMessage.h" /* * Braze Public API: ABKInAppMessageHTMLBase */ NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessageHTMLBase : ABKInAppMessage /*! * This is the local URL of the assets directory for the HTML in-app message. Please note that the * value of this property can be overridden by Braze at the time of displaying, so please don't set * it as the value will be discarded. */ @property (strong, nonatomic) NSURL *assetsLocalDirectoryPath; /*! * Log a click on the in-app message with a buttonId. HTMLFull in-app messages have the limitation of only * handling a single button click, but we allow HTML in-app messages to handle multiple button clicks. * * @param buttonId the id of the click */ - (void)logInAppMessageHTMLClickWithButtonID:(NSString *)buttonId; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKInAppMessageHTMLFull.h ================================================ #import #import "ABKInAppMessageHTMLBase.h" /* * Braze Public API: ABKInAppMessageHTMLFull */ NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessageHTMLFull : ABKInAppMessageHTMLBase /*! * This property is the remote URL of the assets zip file. */ @property (strong, nonatomic, nullable) NSURL *assetsZipRemoteUrl; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKInAppMessageImmersive.h ================================================ #import "ABKInAppMessage.h" @class ABKInAppMessageButton; /* * Braze Public API: ABKInAppMessageImmersive */ NS_ASSUME_NONNULL_BEGIN /*! * The ABKInAppMessageImmersiveImageStyle defines the image style of the in-app message * * ABKInAppMessageGraphic - The image will make up the entire in-app message, with buttons on the * image(buttons are optional). No icons, headers or message will be displayed in this style. * * * ABKInAppMessageTopImage - This is the default image style. The image will be on upper top of the * in-app message if there is one, with all other in-app message elements. */ typedef NS_ENUM(NSInteger, ABKInAppMessageImmersiveImageStyle) { ABKInAppMessageGraphic, ABKInAppMessageTopImage }; @interface ABKInAppMessageImmersive : ABKInAppMessage /*! * header defines the header text of the in-app message. * The header will only be displayed in one line on the default Braze in-app messages. If the header is more than one * line, it will be truncated at the end. */ @property (copy, nullable) NSString *header; /*! * headerTextColor defines the header text color, when there is a header string in the in-app message. The default text color * is black. */ @property (nonatomic, strong, nullable) UIColor *headerTextColor; /*! * closeButtonColor defines the close button color of the in-app message. * When this property is nil, the close button's default color is black. */ @property (nonatomic, strong, nullable) UIColor *closeButtonColor; /*! * buttons defines the buttons of the in-app message. * Each button must be an instance of ABKInAppMessageButton. * When there are more than two buttons in the array, only the first two buttons will be displayed in the in-app message. * For more information and setting of ABKInAppMessageButton, please see the documentation in ABKInAppMessageButton.h for additional details. */ @property (readonly, copy, nullable) NSArray *buttons; /*! * frameColor defines the frame color of an immersive in-app message. This color will fill the * screen outside of the in-app message. When the property is nil, the color will be * set to the default color, which is black with 90% opacity. */ @property (nonatomic, strong, nullable) UIColor *frameColor; /*! * headerTextAlignment defines the preferred text alignment of the header label. * The default value is NSTextAlignmentCenter. */ @property NSTextAlignment headerTextAlignment; /*! * imageStyle defines the image style of a immersive in-app message. * For more information about the possible image styles, please check the documentation of * ABKInAppMessageImmersiveImageStyle above. */ @property ABKInAppMessageImmersiveImageStyle imageStyle; /*! * @param buttonId The clicked button's button ID for the in-app message. This number can't be negative. * If you're handling in-app messages completely on your own, you should still report * clicks on the in-app message button back to Braze with this method so that your campaign reporting features * still work in the dashboard. * * Note: Each in-app message can log at most one button click. */ - (void)logInAppMessageClickedWithButtonID:(NSInteger)buttonId; /*! * @param buttonArray The button array for the in-app message. This array should NOT be nil nor empty. Every object in the array * must be an instance of ABKInAppMessageButton. * * This method will set the in-app message buttons. */ - (void)setInAppMessageButtons:(NSArray *)buttonArray; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKInAppMessageModal.h ================================================ #import "ABKInAppMessageImmersive.h" /* * Braze Public API: ABKInAppMessageModal */ NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessageModal : ABKInAppMessageImmersive @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKInAppMessageSlideup.h ================================================ #import "ABKInAppMessage.h" /*! * There are two possible values which control where the in-app message will enter the view. * * ABKInAppMessageSlideupFromBottom - This is the default behavior. * The in-app message will slide onto the screen from the bottom edge of the view and will hide by sliding back down off * the bottom of the screen. * * ABKInAppMessageSlideupFromTop - The in-app message will slide onto the screen from the top edge of the view and will hide by sliding * back up off the top of the screen. */ typedef NS_ENUM(NSInteger, ABKInAppMessageSlideupAnchor) { ABKInAppMessageSlideupFromTop, ABKInAppMessageSlideupFromBottom }; /* * Braze Public API: ABKInAppMessageSlideup */ NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessageSlideup : ABKInAppMessage /*! * If hideChevron equals YES, the in-app message will not render the chevron on the right side of the in-app message. * The chevron is a useful visual cue for the user that more content may be reached by tapping the in-app message. */ @property BOOL hideChevron; /*! * inAppMessageSlideupAnchor defines the position of the in-app message on screen. * See the above documentation for ABKInAppMessageAnchor enum documentation above offers additional details. */ @property ABKInAppMessageSlideupAnchor inAppMessageSlideupAnchor; /*! * chevronColor defines the chevron arrow color of the in-app message. * When this property is nil, the chevron's default color is white. */ @property (strong, nullable) UIColor *chevronColor; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKInAppMessageUIControlling.h ================================================ #import #import "ABKInAppMessage.h" #import "ABKInAppMessageControllerDelegate.h" @protocol ABKInAppMessageUIControlling @optional /*! * @discussion This method sets the optional ABKInAppMessageUIDelegate. * * To set this delegate, call [[Appboy sharedInstance].inAppMessageController.inAppMessageUIController * setInAppMessageUIDelegate: ] after initializing Braze. */ - (void)setInAppMessageUIDelegate:(id)uiDelegate; /*! * @discussion This method will hide the in-app message that is currently being displayed. * The animated parameter controls whether or not the in-app message will be animated * away. This method does nothing if no in-app * message is currently being displayed. * * Note: This will not fire the onInAppMessageDismissed: delegate method. * * For customization, please use a subclass or category to override this method. */ - (void)hideCurrentInAppMessage:(BOOL)animated; /*! * @discussion This method will return the ABKInAppMessageDisplayChoice (see ABKInAppMessageControllerDelegate * for more information) based on whether or not the keyboard is showing. * If you have implemented the beforeInAppMessageDisplayed:withKeyboardIsUp: in * ABKInAppMessageUIDelegate, the choice returned there will override the default choice. * * For customization, please use a subclass or category to override this method. */ - (ABKInAppMessageDisplayChoice)getCurrentDisplayChoiceForInAppMessage:(ABKInAppMessage *)inAppMessage; /*! * @discussion This method will return the ABKInAppMessageDisplayChoice (see ABKInAppMessageControllerDelegate * for more information) based on whether or not the keyboard is showing. * * For customization, please use a subclass or category to override this method. */ - (ABKInAppMessageDisplayChoice)getCurrentDisplayChoiceForControlInAppMessage:(ABKInAppMessage *)controlInAppMessage; /*! * @discussion This method displays the in-app message. We call it when the in-app message has no * image URL, or there is an image URL, and it has already been downloaded. If you call * this method directly and the image hasn't been downloaded, there will be a spinner * animating in the image view. * * For customization, please use a subclass or category to override this method. */ - (void)showInAppMessage:(ABKInAppMessage *)inAppMessage; /*! * @discussion This method returns whether or not an in-app message is currently being shown. * * For customization, please use a subclass or category to override this method. */ - (BOOL)inAppMessageCurrentlyVisible; @end ================================================ FILE: AppboyKit/include/ABKInAppMessageWebViewBridge.h ================================================ #import #import "ABKInAppMessageHTML.h" NS_ASSUME_NONNULL_BEGIN @class Appboy; @class ABKInAppMessageHTML; @protocol ABKInAppMessageWebViewBridgeDelegate; #pragma mark - ABKInAppMessageWebViewBridge /*! * The webview bridge * @discussion The bridge is automatically setup on initialization and destroyed on dealloc. The bridge * needs to be retained to stay enabled. Keep a strong instance of the bridge in a property to do so */ @interface ABKInAppMessageWebViewBridge : NSObject /*! * The delegate instance */ @property (nonatomic, weak) id delegate; /*! * Initialize an instance of ABKInAppMessageWebViewBridge * @param webView The WKWebView in which the bridge needs to be setup * @param inAppMessage The InAppMessage being displayed * @param appboy The Appboy instance */ - (instancetype)initWithWebView:(WKWebView *)webView inAppMessage:(ABKInAppMessageHTML *)inAppMessage appboyInstance:(Appboy *)appboy; @end #pragma mark - ABKInAppMessageWebViewBridgeDelegate /*! * Methods for managing bridge related actions */ @protocol ABKInAppMessageWebViewBridgeDelegate /*! * Tells the delegate that the bridge has received a click action to execute * @param webViewBridge The bridge informing the delegate * @param clickAction The clickAction performed */ - (void)webViewBridge:(ABKInAppMessageWebViewBridge *)webViewBridge receivedClickAction:(ABKInAppMessageClickActionType)clickAction; /*! * Tells the delegate that a close message action was received * @param webViewBridge The bridge informing the delegate */ - (void)closeMessageWithWebViewBridge:(ABKInAppMessageWebViewBridge *)webViewBridge; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKLocationManager.h ================================================ #import #import @class ABKServerConfig; NS_ASSUME_NONNULL_BEGIN @interface ABKLocationManager : NSObject /*! * Use ABKEnableAutomaticLocationCollectionKey to enable automatic location tracking. * For more information, please refer to Appboy.h. */ @property (readonly) BOOL enableLocationTracking; /*! * Use ABKEnableGeofencesKey to enable geofences. * For more information, please refer to Appboy.h. */ @property (readonly) BOOL enableGeofences; /*! * Use ABKDisableAutomaticGeofenceRequestsKey to disable automatic geofence requests. * For more information, please refer to requestGeofencesWithLongitude:latitude: in Appboy.h */ @property (readonly) BOOL disableAutomaticGeofenceRequests; /*! * Calling this method will log a location using the regular location provider if a location is reported in under * 60 seconds. After 60 seconds expires the regular location provider will stop collecting location. */ - (void)logSingleLocation; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKLocationManagerProvider.h ================================================ #import /*! * Do not call these methods within your code. They are meant for Braze internal use only. */ /*! * ABKLocationManagerProvider.h and ABKLocationManagerProvider.m must be added to your project * regardless of whether or not you enable location services. This occurs automatically if you integrate/update via the CocoaPod. */ /* * Braze Public API: ABKLocationManagerProvider */ @class CLLocationManager; NS_ASSUME_NONNULL_BEGIN @interface ABKLocationManagerProvider : NSObject + (BOOL)locationServicesEnabled; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKModalWebViewController.h ================================================ #import #import @interface ABKModalWebViewController : UINavigationController /*! * The url the modal web view controller should open. Please note that this is the initial url and * won't be updated if the initial url re-directs to another url. */ @property NSURL *url; /*! * The WKWebView which displays the web view. */ @property (nonatomic) IBOutlet WKWebView *webView; /*! * The UIProgressView which shows the web view loading process. It will be on top of the web view and * will disappear as soon as the page is loaded. */ @property (nonatomic) IBOutlet UIProgressView *progressBar; @end ================================================ FILE: AppboyKit/include/ABKNoConnectionLocalization.h ================================================ #import @interface ABKNoConnectionLocalization : NSObject + (NSString *)getNoConnectionLocalizedString; @end ================================================ FILE: AppboyKit/include/ABKPushUtils.h ================================================ #if !TARGET_OS_TV #import #import #import NS_ASSUME_NONNULL_BEGIN /* * Braze Public API: ABKPushUtils */ @interface ABKPushUtils : NSObject /*! * @param response The UNNotificationResponse passed to userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:. * * @return YES if the user notification was sent from Braze servers. */ + (BOOL)isAppboyUserNotification:(UNNotificationResponse *)response API_AVAILABLE(ios(10.0), macCatalyst(14.0)); /*! * @param userInfo The userInfo dictionary passed to application:didReceiveRemoteNotification:fetch​Completion​Handler: * or application:didReceiveRemoteNotification:. * * @return YES if the push notification was sent from Braze servers. */ + (BOOL)isAppboyRemoteNotification:(NSDictionary *)userInfo; /*! * @param userInfo The userInfo dictionary passed to application:didReceiveRemoteNotification:fetchCompletionHandler: * or application:didReceiveRemoteNotification:. * * @return YES if the push notification was sent by Braze for an internal feature. * * @discussion Braze uses content-available silent notifications for internal features. You can use this method to ensure * your app doesn't take any undesired or unnecessary actions upon receiving Braze's internal content-available notifications * (e.g., pinging your server for content). */ + (BOOL)isAppboyInternalRemoteNotification:(NSDictionary *)userInfo; /*! * @param response The UNNotificationResponse passed to userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:. * * @return YES if the user notification was sent by Braze for uninstall tracking. * * @discussion Uninstall tracking notifications are content-available silent notifications. You can use this method to ensure * your app doesn't take any undesired or unnecessary actions upon receiving Braze's uninstall tracking notifications * (e.g., pinging your server for content). */ + (BOOL)isUninstallTrackingUserNotification:(UNNotificationResponse *)response API_AVAILABLE(ios(10.0), macCatalyst(14.0)); /*! * @param userInfo The userInfo dictionary passed to application:didReceiveRemoteNotification:fetchCompletionHandler: * or application:didReceiveRemoteNotification:. * * @return YES if the push notification was sent by Braze for uninstall tracking. * * @discussion Uninstall tracking notifications are content-available silent notifications. You can use this method to ensure * your app doesn't take any undesired or unnecessary actions upon receiving Braze's uninstall tracking notifications * (e.g., pinging your server for content). */ + (BOOL)isUninstallTrackingRemoteNotification:(NSDictionary *)userInfo; /*! * @param response The UNNotificationResponse passed to userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:. * * @return YES if the user notification was sent by Braze for syncing geofences. * * @discussion Geofence sync notifications are content-available silent notifications. You can use this method to ensure * your app doesn't take any undesired or unnecessary actions upon receiving Braze's geofence sync notifications * (e.g., pinging your server for content). */ + (BOOL)isGeofencesSyncUserNotification:(UNNotificationResponse *)response API_AVAILABLE(ios(10.0), macCatalyst(14.0)); /*! * @param userInfo The userInfo dictionary passed to application:didReceiveRemoteNotification:fetchCompletionHandler: * or application:didReceiveRemoteNotification:. * * @return YES if the push notification was sent by Braze for syncing geofences. * * @discussion Geofence sync notifications are content-available silent notifications. You can use this method to ensure * your app doesn't take any undesired or unnecessary actions upon receiving Braze's geofence sync notifications * (e.g., pinging your server for content). */ + (BOOL)isGeofencesSyncRemoteNotification:(NSDictionary *)userInfo; /*! * @param userInfo The userInfo dictionary passed to application:didReceiveRemoteNotification:fetch​Completion​Handler: * * @return YES if the push notification was sent by Braze and is silent. */ + (BOOL)isAppboySilentRemoteNotification:(NSDictionary *)userInfo; /*! * @param userInfo The userInfo dictionary passed to application:didReceiveRemoteNotification:fetchCompletionHandler: * or application:didReceiveRemoteNotification:. * * @return YES if the push notification was sent by Braze for push stories. */ + (BOOL)isPushStoryRemoteNotification:(NSDictionary *)userInfo; + (BOOL)notificationContainsContentCard:(NSDictionary *)userInfo; /*! * @param userInfo The userInfo dictionary payload. * * @return YES if the notification contains an a flag that inticates the device should fetch test triggers from the server. * */ + (BOOL)shouldFetchTestTriggersFlagContainedInPayload:(NSDictionary *)userInfo __deprecated; /*! * @return A set of the default UNNotificationCategories used by Braze. */ + (NSSet *)getAppboyUNNotificationCategorySet API_AVAILABLE(ios(10.0), macCatalyst(14.0)); + (NSSet *)getAppboyUIUserNotificationCategorySet __deprecated_msg("Please use `getAppboyUNNotificationCategorySet` instead."); @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: AppboyKit/include/ABKSDWebImageProxy.h ================================================ #import NS_ASSUME_NONNULL_BEGIN static NSString *const CORE_VERSION_WARNING = @"Attempting to download image but Braze image utilities not found. Make sure you chose the UI Subspec if you want to use Braze's UI."; /* * This proxy class gives the Braze iOS SDK access to the SDWebImage framework. * * NOTE: * This class requires SDWebImage version 4.0*. */ @interface ABKSDWebImageProxy : NSObject + (void)setImageForView:(UIImageView *)imageView showActivityIndicator:(BOOL)showActivityIndicator withURL:(nullable NSURL *)imageURL imagePlaceHolder:(nullable UIImage *)placeHolder completed:(nullable void (^)(UIImage * _Nullable image, NSError * _Nullable error, NSInteger cacheType, NSURL * _Nullable imageURL))completion; + (void)loadImageWithURL:(nullable NSURL *)url options:(NSInteger)options completed:(nullable void(^)(UIImage *image, NSData *data, NSError *error, NSInteger cacheType, BOOL finished, NSURL *imageURL))completion; + (void)diskImageExistsForURL:(nullable NSURL *)url completed:(nullable void (^)(BOOL isInCache))completion; + (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url; + (void)removeSDWebImageForKey:(nullable NSString *)key; + (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key; + (void)clearSDWebImageCache; + (BOOL)isSupportedSDWebImageVersion; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKSdkAuthenticationDelegate.h ================================================ #import #import "ABKSdkAuthenticationError.h" /* * Braze Public API: ABKSdkAuthenticationDelegate */ NS_ASSUME_NONNULL_BEGIN @protocol ABKSdkAuthenticationDelegate /*! * This method is fired when an SDK Authentication error is returned by the server, for example, if * the signature is expired or invalid. * * You are responsible for providing the Braze SDK a valid signature when this delegate method is * called. * SDK requests will retry periodically using an exponential backoff approach. After 50 consecutive * failed attempts, retries will be paused until the next session start. * * @param authError The SDK Authentication error returned by the server */ - (void)handleSdkAuthenticationError:(ABKSdkAuthenticationError *)authError; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKSdkAuthenticationError.h ================================================ #import /* * Braze Public API: ABKSdkAuthenticationError */ NS_ASSUME_NONNULL_BEGIN @interface ABKSdkAuthenticationError : NSObject /*! * The error code for the SDK Authentication failure. */ @property (readonly) NSInteger code; /*! * The reason for the SDK Authentication failure. */ @property (nullable, readonly) NSString *reason; /*! * The user ID associated with the request that failed due to SDK Authentication failure. */ @property (nullable, readonly) NSString *userId; /*! * The signature that was sent with the request that failed due to SDK Authentication failure. */ @property (readonly) NSString *signature; - (instancetype)initWithCode:(NSInteger)code reason:(NSString *)reason userId:(NSString *)userId signature:(NSString *)signature; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKSdkMetadata.h ================================================ /*! * Enum representing the accepted SDK Metatadata. * See addSdkMetadata for more details. */ typedef NSString *ABKSdkMetadata NS_TYPED_EXTENSIBLE_ENUM; extern ABKSdkMetadata const ABKSdkMetadataAdjust; extern ABKSdkMetadata const ABKSdkMetadataAirBridge; extern ABKSdkMetadata const ABKSdkMetadataAppsFlyer; extern ABKSdkMetadata const ABKSdkMetadataBluedot; extern ABKSdkMetadata const ABKSdkMetadataBranch; extern ABKSdkMetadata const ABKSdkMetadataCordova; extern ABKSdkMetadata const ABKSdkMetadataCarthage; extern ABKSdkMetadata const ABKSdkMetadataCocoaPods; extern ABKSdkMetadata const ABKSdkMetadataCordovaPM; extern ABKSdkMetadata const ABKSdkMetadataExpo; extern ABKSdkMetadata const ABKSdkMetadataFoursquare; extern ABKSdkMetadata const ABKSdkMetadataFlutter; extern ABKSdkMetadata const ABKSdkMetadataGoogleTagManager; extern ABKSdkMetadata const ABKSdkMetadataGimbal; extern ABKSdkMetadata const ABKSdkMetadataGraddle; extern ABKSdkMetadata const ABKSdkMetadataIonic; extern ABKSdkMetadata const ABKSdkMetadataKochava; extern ABKSdkMetadata const ABKSdkMetadataManual; extern ABKSdkMetadata const ABKSdkMetadataMParticle; extern ABKSdkMetadata const ABKSdkMetadataNativeScript; extern ABKSdkMetadata const ABKSdkMetadataNPM; extern ABKSdkMetadata const ABKSdkMetadataNuGet; extern ABKSdkMetadata const ABKSdkMetadataPub; extern ABKSdkMetadata const ABKSdkMetadataRadar; extern ABKSdkMetadata const ABKSdkMetadataReactNative; extern ABKSdkMetadata const ABKSdkMetadataSegment; extern ABKSdkMetadata const ABKSdkMetadataSingular; extern ABKSdkMetadata const ABKSdkMetadataSwiftPM; extern ABKSdkMetadata const ABKSdkMetadataTealium; extern ABKSdkMetadata const ABKSdkMetadataUnreal; extern ABKSdkMetadata const ABKSdkMetadataUnityPM; extern ABKSdkMetadata const ABKSdkMetadataUnity; extern ABKSdkMetadata const ABKSdkMetadataVizbee; extern ABKSdkMetadata const ABKSdkMetadataXamarin; ================================================ FILE: AppboyKit/include/ABKTextAnnouncementCard.h ================================================ #import "ABKCard.h" /* * Braze Public API: ABKTextAnnouncementCard */ NS_ASSUME_NONNULL_BEGIN @interface ABKTextAnnouncementCard : ABKCard /* * The title text for the card. */ @property (copy) NSString *title; /* * The description text for the card. */ @property (copy) NSString *cardDescription; /* * The link text for the property url, like @"blog.appboy.com". It can be displayed on the card's * UI to indicate the action/direction of clicking on the card. */ @property (copy, nullable) NSString *domain; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKTwitterUser.h ================================================ #import /* * Braze Public API: ABKTwitterUser */ NS_ASSUME_NONNULL_BEGIN @interface ABKTwitterUser : NSObject /*! * The value returned from Twitter's Users API with key "description". Please refer to * https://dev.twitter.com/overview/api/users for more information. */ @property (copy, nullable) NSString* userDescription; /*! * The value returned from Twitter's Users API with key "name". Please refer to * https://dev.twitter.com/overview/api/users for more information. */ @property (copy, nullable) NSString* twitterName; /*! * The value returned from Twitter's Users API with key "profile_image_url". Please refer to * https://dev.twitter.com/overview/api/users for more information. */ @property (copy, nullable) NSString* profileImageUrl; /*! * The value returned from Twitter's Users API with key "screen_name". Please refer to * https://dev.twitter.com/overview/api/users for more information. */ @property (copy, nullable) NSString* screenName; /*! * The value returned from Twitter's Users API with key "followers_count". Please refer to * https://dev.twitter.com/overview/api/users for more information. */ @property NSInteger followersCount; /*! * The value returned from Twitter's Users API with key "friends_count". Please refer to * https://dev.twitter.com/overview/api/users for more information. */ @property NSInteger friendsCount; /*! * The value returned from Twitter's Users API with key "statuses_count". Please refer to * https://dev.twitter.com/overview/api/users for more information. */ @property NSInteger statusesCount; /*! * The value returned from Twitter's Users API with key "id". Please refer to * https://dev.twitter.com/overview/api/users for more information. */ @property NSInteger twitterID; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKURLDelegate.h ================================================ #import #import "Appboy.h" /* * Braze Public API: ABKURLDelegate */ NS_ASSUME_NONNULL_BEGIN @protocol ABKURLDelegate /*! * @param url The deep link or web URL being offered to the delegate method. * @param channel An enum representing the URL's associated messaging channel. * @param extras The extras dictionary associated with the campaign or messaging object that the URL originated from. Extras may be specified as key-value pairs on the Braze dashboard. * @return Boolean value which controls whether or not Braze will handle opening the URL. Returning YES will * prevent Braze from opening the URL. Returning NO will cause Braze to handle opening the URL. * * This delegate method is fired whenever the user attempts to open a URL sent by Braze. You can use this delegate * to customize Braze's URL handling. */ - (BOOL)handleAppboyURL:(NSURL * _Nullable)url fromChannel:(ABKChannel)channel withExtras:(NSDictionary * _Nullable)extras; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/ABKUser.h ================================================ // // ABKUser.h // AppboySDK #import @class ABKFacebookUser; @class ABKTwitterUser; @class ABKAttributionData; NS_ASSUME_NONNULL_BEGIN /* ------------------------------------------------------------------------------------------------------ * Enums */ /*! * Genders recognized by the SDK. */ typedef NS_ENUM(NSInteger, ABKUserGenderType) { ABKUserGenderMale, ABKUserGenderFemale, ABKUserGenderOther, ABKUserGenderUnknown, ABKUserGenderNotApplicable, ABKUserGenderPreferNotToSay }; /*! * Convenience enum to represent notification status, for email and push notifications. * * OPTED_IN: subscribed, and explicitly opted in. * SUBSCRIBED: subscribed, but not explicitly opted in. * UNSUBSCRIBED: unsubscribed and/or explicitly opted out. */ typedef NS_ENUM(NSInteger, ABKNotificationSubscriptionType) { ABKOptedIn, ABKSubscribed, ABKUnsubscribed }; /*! * When setting the custom attributes with custom keys: * 1. The maximum key length is 255 characters; longer keys are truncated. * 2. The maximum length for a string value in a custom attribute is 255 characters; longer values are truncated. */ /* * Braze Public API: ABKUser */ @interface ABKUser : NSObject /*! * The User's first name (String) */ @property (nonatomic, copy, nullable) NSString *firstName; /*! * The User's last name (String) */ @property (nonatomic, copy, nullable) NSString *lastName; /*! * The User's email (String) */ @property (nonatomic, copy, nullable) NSString *email; /*! * The User's date of birth (NSDate) */ @property (nonatomic, copy, nullable) NSDate *dateOfBirth; /*! * The User's country (String) */ @property (nonatomic, copy, nullable) NSString *country; /*! * The User's home city (String) */ @property (nonatomic, copy, nullable) NSString *homeCity; /*! * The User's language (String) * * Language Strings should be valid ISO 639-1 language codes. * See https://www.loc.gov/standards/iso639-2/php/code_list.php. * * If not set here, user language will be inferred from the device language. */ @property (nonatomic, copy, nullable) NSString *language; /*! * The User's phone number (String) */ @property (nonatomic, copy, nullable) NSString *phone; @property (nonatomic, copy, nullable, readonly) NSString *userID; /*! * The User's avatar image URL. This URL will be processed by the server and used in their user profile on the * dashboard. (String) */ @property (nonatomic, copy, nullable) NSString *avatarImageURL; /*! * The User's Facebook account information. For more detail, please refer to ABKFacebookUser.h. */ @property (strong, nullable) ABKFacebookUser *facebookUser; /*! * The User's Twitter account information. For more detail, please refer to ABKTwitterUser.h. */ @property (strong, nullable) ABKTwitterUser *twitterUser; /*! * Sets the attribution information for the user. For in apps that have an install tracking integration. * For more information, please refer to ABKAttributionData.h. */ @property (strong, nullable) ABKAttributionData *attributionData; /*! * Adds an an alias for the current user. Individual (alias, label) pairs can exist on one and only one user. * If a different user already has this alias or external user id, the alias attempt will be rejected * on the server. * * @param alias The alias of the current user. * @param label The label of the alias; used to differentiate it from other aliases for the user. * @return Whether or not the alias and label are valid. Does not guarantee they won't collide with * an existing pair. */ - (BOOL)addAlias:(NSString *)alias withLabel:(NSString *)label; /*! * @param gender ABKUserGender enum representing the user's gender. * @return YES if the user gender is set properly */ - (BOOL)setGender:(ABKUserGenderType)gender; /*! * Sets whether or not the user should be sent email campaigns. Setting it to unsubscribed opts the user out of * an email campaign that you create through the Braze dashboard. * * @param emailNotificationSubscriptionType enum representing the user's email notifications subscription type. * @return YES if the field is set successfully, else NO. */ - (BOOL)setEmailNotificationSubscriptionType:(ABKNotificationSubscriptionType)emailNotificationSubscriptionType; /*! * Sets the push notification subscription status of the user. Used to collect information about the user. * * @param pushNotificationSubscriptionType enum representing the user's push notifications subscription type. * @return YES if the field is set successfully, else NO. */ - (BOOL)setPushNotificationSubscriptionType:(ABKNotificationSubscriptionType)pushNotificationSubscriptionType; /*! * Adds the user to a Subscription Group. * * @param groupId The string UUID corresponding to the subscription group, provided by the Braze dashboard. * @return YES if the user was successfully added, else NO. If not, the groupId might have been nil or invalid. */ - (BOOL)addToSubscriptionGroupWithGroupId:(NSString *)groupId; /*! * Removes the user from a Subscription Group. * * @param groupId The string UUID corresponding to the subscription group, provided by the Braze dashboard. * @return YES if the user was successfully removed, else NO. If not, the groupId might have been nil or invalid. */ - (BOOL)removeFromSubscriptionGroupWithGroupId:(NSString *)groupId; /*! * @param key The String name of the custom user attribute * @param value A boolean value to set as a custom attribute * @return whether or not the custom user attribute was set successfully; If not, your key might have been nil or empty, * your value might have been invalid (either nil, or not of the correct type), or you tried to set a value for * one of the reserved keys. Please check the log for more details about the specific failure you encountered. */ - (BOOL)setCustomAttributeWithKey:(NSString *)key andBOOLValue:(BOOL)value; /*! * @param key The String name of the custom user attribute * @param value An integer value to set as a custom attribute * @return whether or not the custom user attribute was set successfully; If not, your key might have been nil or empty, * your value might have been invalid (either nil, or not of the correct type), or you tried to set a value for * one of the reserved keys. Please check the log for more details about the specific failure you encountered. */ - (BOOL)setCustomAttributeWithKey:(NSString *)key andIntegerValue:(NSInteger)value; /*! * @param key The String name of the custom user attribute * @param value A double value to set as a custom attribute * @return whether or not the custom user attribute was set successfully; If not, your key might have been nil or empty, * your value might have been invalid (either nil, or not of the correct type), or you tried to set a value for * one of the reserved keys. Please check the log for more details about the specific failure you encountered. */ - (BOOL)setCustomAttributeWithKey:(NSString *)key andDoubleValue:(double)value; /*! * @param key The String name of the custom user attribute * @param value An NSString value to set as a custom attribute * @return whether or not the custom user attribute was set successfully; If not, your key might have been nil or empty, * your value might have been invalid (either nil, or not of the correct type), or you tried to set a value for * one of the reserved keys. Please check the log for more details about the specific failure you encountered. */ - (BOOL)setCustomAttributeWithKey:(NSString *)key andStringValue:(NSString *)value; /*! * @param key The String name of the custom user attribute * @param value An NSDate value to set as a custom attribute * @return whether or not the custom user attribute was set successfully; If not, your key might have been nil or empty, * your value might have been invalid (either nil, or not of the correct type), or you tried to set a value for * one of the reserved keys. Please check the log for more details about the specific failure you encountered. */ - (BOOL)setCustomAttributeWithKey:(NSString *)key andDateValue:(NSDate *)value; /*! * @param key The String name of the custom user attribute to unset * @return whether or not the custom user attribute was unset successfully */ - (BOOL)unsetCustomAttributeWithKey:(NSString *)key; /** * Increments the value of an custom attribute by one. Only integer and long custom attributes can be incremented. * Attempting to increment a custom attribute that is not an integer or a long will be ignored. If you increment a * custom attribute that has not previously been set, a custom attribute will be created and assigned a value of one. * * @param key The identifier of the custom attribute * @return YES if the increment for the custom attribute of given key is saved */ - (BOOL)incrementCustomUserAttribute:(NSString *)key; /** * Increments the value of an custom attribute by a given amount. Only integer and long custom attributes can be * incremented. Attempting to increment a custom attribute that is not an integer or a long will be ignored. If * you increment a custom attribute that has not previously been set, a custom attribute will be created and assigned * the value of incrementValue. To decrement the value of a custom attribute, use a negative incrementValue. * * @param key The identifier of the custom attribute * @param incrementValue The amount by which to increment the custom attribute * @return YES if the increment for the custom attribute of given key is saved */ - (BOOL)incrementCustomUserAttribute:(NSString *)key by:(NSInteger)incrementValue; /** * Adds the string value to a custom attribute string array specified by the key. If you add a key that has not * previously been set, a custom attribute string array will be created containing the value. * * @param key The custom attribute key * @param value A string to be added to the custom attribute string array * @return YES if the operation was successful */ - (BOOL)addToCustomAttributeArrayWithKey:(NSString *)key value:(NSString *)value; /** * Removes the string value from a custom attribute string array specified by the key. If you remove a key that has not * previously been set, nothing will be changed. * * @param key The custom attribute key * @param value A string to be removed from the custom attribute string array * @return YES if the operation was successful */ - (BOOL)removeFromCustomAttributeArrayWithKey:(NSString *)key value:(NSString *)value; /** * Sets a string array from a custom attribute specified by the key. * * @param key The custom attribute key * @param valueArray A string array to set as a custom attribute. If this value is nil, then Braze will unset the custom * attribute and remove the corresponding array if there is one. * @return YES if the operation was successful */ - (BOOL)setCustomAttributeArrayWithKey:(NSString *)key array:(nullable NSArray *)valueArray; /*! * Sets the last known location for the user. Intended for use with ABKDisableLocationAutomaticTrackingOptionKey set to YES * when starting Braze, so that the only locations being set are by the integrating app. Otherwise, calls to this * method will be contending with automatic location update events. * * @param latitude The latitude of the User's location in degrees, the number should be in the range of [-90, 90] * @param longitude The longitude of the User's location in degrees, the number should be in the range of [-180, 180] * @param horizontalAccuracy The accuracy of the User's horizontal location in meters, the number should not be negative */ - (BOOL)setLastKnownLocationWithLatitude:(double)latitude longitude:(double)longitude horizontalAccuracy:(double)horizontalAccuracy; /*! * Sets the last known location for the user. Intended for use with ABKDisableLocationAutomaticTrackingOptionKey set to YES * when starting Braze, so that the only locations being set are by the integrating app. Otherwise, calls to this * method will be contending with automatic location update events. * * @param latitude The latitude of the User's location in degrees, the number should be in the range of [-90, 90] * @param longitude The longitude of the User's location in degrees, the number should be in the range of [-180, 180] * @param horizontalAccuracy The accuracy of the User's horizontal location in meters, the number should not be negative * @param altitude The altitude of the User's location in meters * @param verticalAccuracy The accuracy of the User's vertical location in meters, the number should not be negative */ - (BOOL)setLastKnownLocationWithLatitude:(double)latitude longitude:(double)longitude horizontalAccuracy:(double)horizontalAccuracy altitude:(double)altitude verticalAccuracy:(double)verticalAccuracy; /*! * Adds the location custom attribute for the user. * * @param key The custom attribute key * @param latitude The latitude of the location in degrees, the number should be in the range of [-90, 90] * @param longitude The longitude of the location in degrees, the number should be in the range of [-180, 180] */ - (BOOL)addLocationCustomAttributeWithKey:(NSString *)key latitude:(double)latitude longitude:(double)longitude; /*! * Removes the location custom attribute for the user. * * @param key The custom attribute key */ - (BOOL)removeLocationCustomAttributeWithKey:(NSString *)key; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/Appboy.h ================================================ // // Appboy.h // AppboySDK /*! \mainpage This site contains technical documentation for the %Braze iOS SDK. Click on the "Classes" link above to view the %Braze public interface classes and start integrating the SDK into your app! */ #import #import #import #import "ABKSdkMetadata.h" #ifndef APPBOY_SDK_VERSION #define APPBOY_SDK_VERSION @"4.7.0" #endif #if !TARGET_OS_TV @class ABKInAppMessageController; @class ABKInAppMessage; @class ABKInAppMessageViewController; #endif @class ABKUser; @class ABKFeedController; @class ABKContentCardsController; @class ABKLocationManager; @protocol ABKInAppMessageControllerDelegate; @protocol ABKIDFADelegate; @protocol ABKURLDelegate; @protocol ABKImageDelegate; @protocol ABKSdkAuthenticationDelegate; NS_ASSUME_NONNULL_BEGIN /* ------------------------------------------------------------------------------------------------------ * Keys for Braze startup options */ /*! * If you want to set the request policy at app startup time (useful for avoiding any automatic data requests made by * Braze at startup if you're looking to have full manual control). You can include one of the * ABKRequestProcessingPolicy enum values as the value for the ABKRequestProcessingPolicyOptionKey in the appboyOptions * dictionary. */ extern NSString *const ABKRequestProcessingPolicyOptionKey; /*! * Sets the data flush interval (in seconds). This only has an effect when the request processing mode is set to * ABKAutomaticRequestProcessing (which is the default). Values are converted into NSTimeIntervals and must be greater * than 1.0. */ extern NSString *const ABKFlushIntervalOptionKey; /*! * This key can be set to YES or NO and will configure whether Braze will automatically collect location (if the user permits). * If set to YES, Braze will collect location if authorized. * If it is set to NO or omitted, location will not be recorded for the user unless you manually * call setUserLastKnownLocation on ABKUser. */ extern NSString *const ABKEnableAutomaticLocationCollectionKey; /*! * This key can be set to YES or NO and will configure whether geofences are enabled. * If set to YES, geofences will be enabled. * If set to NO, geofences will be disabled. * If the field is omitted, we will use the value of ABKEnableAutomaticLocationCollectionKey. */ extern NSString *const ABKEnableGeofencesKey; /*! * This key can be set to YES or NO and will configure whether geofence requests are made automatically. * If set to YES, geofence requests will not be made automatically. * If set to NO, geofence requests will be made automatically. This is the default value when you have geofences enabled. */ extern NSString *const ABKDisableAutomaticGeofenceRequestsKey; /*! * This key can be set to an instance of a class that extends ABKIDFADelegate, which can be used to pass advertiser tracking information to to Braze. */ extern NSString *const ABKIDFADelegateKey; /*! * This key can be set to a custom API endpoint. This gets sent in the format sdk.api.braze.eu. */ extern NSString *const ABKEndpointKey; /*! * This key can be set to an instance of a class that conforms to the ABKURLDelegate protocol, allowing it to handle URLs in a custom way. */ extern NSString *const ABKURLDelegateKey; /*! * This can can be set to an instance of a class that conforms to the ABKImageDelegate protocol, allowing flexibility for using custom image libraries. */ extern NSString *const ABKImageDelegateKey; /*! * This key can be set to an instance of a class that conforms to the ABKInAppMessageControllerDelegate protocol, allowing it to handle in-app messages in a custom way. */ extern NSString *const ABKInAppMessageControllerDelegateKey; /*! * This key can be set YES or NO and will configure whether a modal in-app message will be dismissed when the user clicks * outside of the in-app message. * If set to YES, the in-app message will be dismissed. * If set to NO, the in-app message will not be dismissed. This is the default value. */ extern NSString *const ABKEnableDismissModalOnOutsideTapKey; /*! * This key can be set YES or NO and will configure whether the SDK Authentication feature is enabled. */ extern NSString *const ABKEnableSDKAuthenticationKey; /*! * This key can can be set to an instance of a class that conforms to the ABKSdkAuthenticationDelegate protocol, allowing it to handle * SDK Authentication errors. Setting this delegate will cause the delegate method `handleSdkAuthenticationError:` to get called in * the event of an SDK Authentication error. */ extern NSString *const ABKSdkAuthenticationDelegateKey; /*! * Set the time interval for session time out (in seconds). This will affect the case when user has a session shorter than * the set time interval. In that case, the session won't be close even though the user closed the app, but will continue until * it times out. The value should be an integer bigger than 0. */ extern NSString *const ABKSessionTimeoutKey; /*! * Set the minimum time interval in seconds between triggers. After a trigger happens, we will ignore any triggers until * the minimum time interval elapses. The default value is 30s. The minimum valid value is 0s. */ extern NSString *const ABKMinimumTriggerTimeIntervalKey; /*! * Key to report the SDK flavor currently being used. For internal use only. */ extern NSString *const ABKSDKFlavorKey; /*! * Key to specify an allowlist for device fields that are collected by the Braze SDK. * * To specify allowlisted device fields, assign the bitwise `OR` of desired fields to this key. Fields are defined * in `ABKDeviceOptions`. To turn off all fields, set the value of this key to `ABKDeviceOptionNone`. By default, * all fields are collected. */ extern NSString *const ABKDeviceAllowlistKey; /*! * This key is deprecated in favor of ABKDeviceAllowlistKey. See ABKDeviceAllowlistKey for more details. */ extern NSString *const ABKDeviceWhitelistKey __deprecated_msg("ABKDeviceWhitelistKey is deprecated. Please use ABKDeviceAllowlistKey instead."); extern NSString *const ABKEphemeralEventsKey; /*! * This key can be set to a string value representing the app group name for the Push Story Notification * Content extension. This is required for the SDK to fetch data from and handle user interactions * with the Push Story app extension. */ extern NSString *const ABKPushStoryAppGroupKey; /*! * This key can be set to an integer value to specify the level of the log statements output by the Braze SDK. * * The default log level is 8 and will minimally log info. To enable verbose logging for debugging, use log level 0. * * This selection will override any LogLevel value set in the Info.plist. */ extern NSString *const ABKLogLevelKey; /* ------------------------------------------------------------------------------------------------------ * Enums */ /*! * Possible values for the SDK's request processing policies: * ABKAutomaticRequestProcessing (default) - All server communication is handled automatically. This includes flushing * analytics data to the server, updating the feed, and requesting new in-app messages. Braze's * communication policy is to perform immediate server requests when user facing data is required (new in-app messages, * feed refreshes, etc.), and to otherwise perform periodic flushes of new analytics data every few seconds. * The interval between periodic flushes can be set explicitly using the ABKFlushInterval startup option. * ABKAutomaticRequestProcessingExceptForDataFlush - Deprecated. Use ABKManualRequestProcessing. * ABKManualRequestProcessing - The same as ABKAutomaticRequestProcessing, except that updates to * custom attributes and triggering of custom events will not automatically flush to the server. Instead, you * must call requestImmediateDataFlush when you want to synchronize newly updated user data with Braze. Note that * the configuration does not turn off all networking, i.e. requests important to the proper functionality of the Braze * SDK will still occur. * * Regardless of policy, Braze will intelligently combine requests on the request queue to minimize the total number of * requests and their combined payload. */ typedef NS_ENUM(NSInteger, ABKRequestProcessingPolicy) { ABKAutomaticRequestProcessing, ABKManualRequestProcessing, ABKAutomaticRequestProcessingExceptForDataFlush __deprecated_enum_msg("ABKAutomaticRequestProcessingExceptForDataFlush is deprecated. Use ManualRequestProcessing.") = ABKManualRequestProcessing }; /*! * Internal enum used to report the SDK flavor being used. */ typedef NS_ENUM(NSInteger , ABKSDKFlavor) { UNITY = 1, REACT, CORDOVA, XAMARIN, FLUTTER, SEGMENT, MPARTICLE, TEALIUM }; typedef NS_OPTIONS(NSUInteger, ABKDeviceOptions) { ABKDeviceOptionNone = 0, ABKDeviceOptionResolution = (1 << 0), ABKDeviceOptionCarrier = (1 << 1), ABKDeviceOptionLocale = (1 << 2), ABKDeviceOptionModel = (1 << 3), ABKDeviceOptionOSVersion = (1 << 4), // Note: The ABKDeviceOptionIDFV allowlist key currently has no effect. // IDFV is read regardless of allowlist settings due to its // role as the primary device identifier within the Braze system. ABKDeviceOptionIDFV = (1 << 5), ABKDeviceOptionIDFA = (1 << 6), ABKDeviceOptionPushEnabled = (1 << 7), ABKDeviceOptionTimezone = (1 << 8), ABKDeviceOptionPushAuthStatus = (1 << 9), ABKDeviceOptionAdTrackingEnabled = (1 << 10), ABKDeviceOptionPushDisplayOptions = (1 << 11), ABKDeviceOptionAll = ~ABKDeviceOptionNone }; /*! * Possible channels supported by the SDK. */ typedef NS_ENUM(NSInteger, ABKChannel) { ABKPushNotificationChannel, ABKInAppMessageChannel, ABKNewsFeedChannel, ABKContentCardChannel, ABKUnknownChannel __deprecated_enum_msg("ABKUnknownChannel will be removed in a future update.") }; /* * Braze Public API: Appboy */ @interface Appboy : NSObject /* ------------------------------------------------------------------------------------------------------ * Initialization */ /*! * Get the Appboy singleton. Returns nil if accessed before startWithApiKey: called. */ + (nullable Appboy *)sharedInstance; /*! * Get the Appboy singleton. Throws an exception if accessed before startWithApiKey: is called. */ + (nonnull Appboy *)unsafeInstance; /*! * @param apiKey The app's API key * @param application the current app * @param launchOptions The options NSDictionary that you get from application:didFinishLaunchingWithOptions * * @discussion Starts up Braze and tells it that your app is done launching. You should call this * method in your App Delegate application:didFinishLaunchingWithOptions method before calling makeKeyAndVisible, * accessing [Appboy sharedInstance] or otherwise rendering Braze view controllers. Your apiKey comes from * the Braze dashboard where you registered your app. */ + (void)startWithApiKey:(NSString *)apiKey inApplication:(UIApplication *)application withLaunchOptions:(nullable NSDictionary *)launchOptions; /*! * @param apiKey The app's API key * @param application The current app * @param launchOptions The options NSDictionary that you get from application:didFinishLaunchingWithOptions * @param appboyOptions An optional NSDictionary with startup configuration values for Braze. See below * for more information. * * @discussion Starts up Braze and tells it that your app is done launching. You should call this * method in your App Delegate application:didFinishLaunchingWithOptions method before calling makeKeyAndVisible, * accessing [Appboy sharedInstance] or otherwise rendering Braze view controllers. Your apiKey comes from * the Braze dashboard where you registered your app. */ + (void)startWithApiKey:(NSString *)apiKey inApplication:(UIApplication *)application withLaunchOptions:(nullable NSDictionary *)launchOptions withAppboyOptions:(nullable NSDictionary *)appboyOptions; /* ------------------------------------------------------------------------------------------------------ * Properties */ /*! * The current app user. * See ABKUser.h and changeUser:userId below. */ @property (readonly) ABKUser *user; @property (readonly) ABKFeedController *feedController; @property (readonly) ABKContentCardsController *contentCardsController; /*! * The policy regarding processing of network requests by the SDK. See the enumeration values for more information on * possible options. This value can be set at runtime, or can be injected in at startup via the appboyOptions dictionary. * * Any time the request processing policy is set to manual, any scheduled flush of the queue is canceled, but if the * request queue was already processing, the current queue will finish processing. If you need to cancel in flight * requests, you need to call
[[Appboy sharedInstance] shutdownServerCommunication]
. * * Setting the request policy does not automatically cause a flush to occur, it just allows for a flush to be scheduled * the next time an eligible request is enqueued. To force an immediate flush after changing the request processing * policy, invoke
[[Appboy sharedInstance] requestImmediateDataFlush]
. */ @property ABKRequestProcessingPolicy requestProcessingPolicy; /*! * A class extending ABKIDFADelegate can be set to provide the IDFA to Braze. */ @property (nonatomic, strong, nullable) id idfaDelegate; /*! * A class conforming to ABKSdkAuthenticationDelegate can be set to handle SDK Authentication errors. */ @property (nonatomic, strong, nullable) id sdkAuthenticationDelegate; /*! * A custom `NSURLSessionConfiguration` for configuring network session parameters. */ @property (nonatomic, readonly) NSURLSessionConfiguration *urlSessionConfiguration; #if !TARGET_OS_TV /*! * The current in-app message manager. * See ABKInAppMessageController.h. */ @property (readonly) ABKInAppMessageController *inAppMessageController; /*! * The Braze location manager provides access to location related functionality in the Braze SDK. * See ABKLocationManager.h. */ @property (nonatomic, readonly) ABKLocationManager *locationManager; /*! * A class conforming to the ABKURLDelegate protocol can be set to handle URLs in a custom way. */ @property (nonatomic, weak, nullable) id appboyUrlDelegate; /*! * A class conforming to ABKImageDelegate can be set to use a custom image library. */ @property (nonatomic, strong, nullable) id imageDelegate; /*! * Property for internal reporting of SDK flavor. */ @property (nonatomic) ABKSDKFlavor sdkFlavor; #endif /* ------------------------------------------------------------------------------------------------------ * Methods */ /*! * Enqueues a data flush request for the current user and immediately starts processing the network queue. Note that if * the queue already contains another request for the current user, that the new data flush request * will be merged into the already existing request and only one will execute for that user. * * If you're using ABKManualRequestProcessing, you only need to call this when you want to force * an immediate flush of updated user data. */ - (void)requestImmediateDataFlush; - (void)flushDataAndProcessRequestQueue __deprecated_msg("Please use `requestImmediateDataFlush` instead."); /*! * Stops all in flight server communication and enables manual request processing control to ensure that no automatic * network activity occurs. You should usually only call shutdownServerCommunication if the OS is forcing you to stop * background tasks upon exit of your application. To continue normal operation after calling this, you will need to * explicitly set the request processing mode back to your desired state. */ - (void)shutdownServerCommunication; /*! * @param userId The new user's ID (from the host application). * * @discussion * This method changes the user's ID. These user IDs should be private and not easily obtained (e.g. not a plain * email address or username). * * When you first start using Braze on a device, the user is considered "anonymous". You can use this method to * optionally identify a user with a unique ID, which enables the following: * * - If the same user is identified on another device, their user profile, usage history and event history will * be shared across devices. * * - If your app is used by multiple people, you can assign each of them a unique identifier to track them * separately. Only the most recent user on a particular device will receive push notifications and in-app * messages. * * - If you identify a user which has never been identified on another device, the entire history of that user as * an "anonymous" user on this device will be preserved and associated with the newly identified user. * * - However, if you identify a user which *has* been identified on another device, the previous anonymous * history of the user on this device will not be added to the already existing profile for that user. * * - Note that switching from one an anonymous user to an identified user or from one identified user to another is * a relatively costly operation. When you request the * user switch, the current session for the previous user is automatically closed and a new session is started. * Braze will also automatically make a data refresh request to get the news feed, in-app message and other information * for the new user. * * Note: Once you identify a user, you cannot go back to the "anonymous" profile. The transition from anonymous * to identified tracking only happens once because the initial anonymous user receives special treatment * to allow for preservation of their history. We recommend against changing the user id just because your app * has entered a "logged out" state because it separates this device from the user profile and thus you will be * unable to target the previously logged out user with re-engagement campaigns. If you anticipate multiple * users on the same device, but only want to target one of them when your app is in a logged out state, we recommend * separately keeping track of the user ID you want to target while logged out and switching back to * that user ID as part of your app's logout process. */ - (void)changeUser:(NSString *)userId; /*! * @param userId The new user's ID (from the host application) * @param signature The SDK Authentication signature for the user being identified. * * @discussion See documantation for `changeUser:` above */ - (void)changeUser:(NSString *)userId sdkAuthSignature:(nullable NSString *)signature; /*! * @param signature The SDK Authentication signature for the current user * * @discussion Sets the signature used for SDK authentication for the current user. */ - (void)setSdkAuthenticationSignature:(NSString *)signature; /*! * @discussion Unsubscribe from SDK Authentication errors. After this method is called, * the ABKSdkAuthenticationDelegate method `handleSdkAuthenticationError:` will not be called in the event of * an SDK Authentication error. */ - (void)unsubscribeFromSdkAuthenticationErrors; /*! * @param eventName The name of the event to log. * * @discussion Adds an app specific event to event tracking log that's lazily pushed up to the server. Think of * events like counters. That is, each time you log an event, we'll update a counter for that user. Events should be * fairly broad like "beat level 1" or "watched video" instead of something more specific like "watched Katy * Perry's Last Friday Night" so you can create more broad user segments for targeting. * *
 * [[Appboy sharedInstance] logCustomEvent:@"clicked_button"];
 * 
*/ - (void)logCustomEvent:(NSString *)eventName; /*! * @param eventName The name of the event to log. * @param properties An NSDictionary of properties to associate with this purchase. Property keys are non-empty NSString objects with * <= 255 characters and no leading dollar signs. Property values can be NSNumber booleans, integers, floats < 62 bits, NSDate objects, * NSString objects with <= 255 characters, or any JSON Encodable object including NSArray and NSDictionary of the previous data types (nested properties). Total length of encoded properties must be under 50 KB. * * @discussion Adds an app specific event to event tracking log that's lazily pushed up to the server. Think of * events like counters. That is, each time you log an event, we'll update a counter for that user. Events should be * fairly broad like "beat level 1" or "watched video" instead of something more specific like "watched Katy * Perry's Last Friday Night" so you can create more broad user segments for targeting. * *
 * [[Appboy sharedInstance] logCustomEvent:@"clicked_button" properties:@{@"key1":@"val"}];
 * 
*/ - (void)logCustomEvent:(NSString *)eventName withProperties:(nullable NSDictionary *)properties; /*! * This method is equivalent to calling logPurchase:inCurrency:atPrice:withQuantity:andProperties: with a quantity of 1 and nil properties. * Please see logPurchase:inCurrency:atPrice:withQuantity:andProperties: for more information. */ - (void)logPurchase:(NSString *)productIdentifier inCurrency:(NSString *)currencyCode atPrice:(NSDecimalNumber *)price; /*! * This method is equivalent to calling logPurchase:inCurrency:atPrice:withQuantity:andProperties with a quantity of 1. * Please see logPurchase:inCurrency:atPrice:withQuantity:andProperties: for more information. */ - (void)logPurchase:(NSString *)productIdentifier inCurrency:(NSString *)currencyCode atPrice:(NSDecimalNumber *)price withProperties:(nullable NSDictionary *)properties; /*! * This method is equivalent to calling logPurchase:inCurrency:atPrice:withQuantity:andProperties with nil properties. * Please see logPurchase:inCurrency:atPrice:withQuantity:andProperties: for more information. */ - (void)logPurchase:(NSString *)productIdentifier inCurrency:(NSString *)currencyCode atPrice:(NSDecimalNumber *)price withQuantity:(NSUInteger)quantity; /*! * @param productIdentifier A String indicating the product that was purchased. Usually the product identifier in the * iTunes store. * @param currencyCode Currencies should be represented as an ISO 4217 currency code. Prices should * be sent in decimal format, with the same base units as are provided by the SKProduct class. Callers of this method * who have access to the NSLocale object for the purchase in question (which can be obtained from SKProduct listings * provided by StoreKit) can obtain the currency code by invoking: *
[locale objectForKey:NSLocaleCurrencyCode]
* Supported currency symbols include: AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BHD, BIF, * BMD, BND, BOB, BRL, BSD, BTC, BTN, BWP, BYR, BZD, CAD, CDF, CHF, CLF, CLP, CNY, COP, CRC, CUC, CUP, CVE, CZK, DJF, * DKK, DOP, DZD, EEK, EGP, ERN, ETB, EUR, FJD, FKP, GBP, GEL, GGP, GHS, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, * IDR, ILS, IMP, INR, IQD, IRR, ISK, JEP, JMD, JOD, JPY, KES, KGS, KHR, KMF, KPW, KRW, KWD, KYD, KZT, LAK, LBP, LKR, LRD, * LSL, LTL, LVL, LYD, MAD, MDL, MGA, MKD, MMK, MNT, MOP, MRO, MTL, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, * NZD, OMR, PAB, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RUB, RWF, SAR, SBD, SCR, SDG, SEK, SGD, SHP, SLL, SOS, SRD, * STD, SVC, SYP, SZL, THB, TJS, TMT, TND, TOP, TRY, TTD, TWD, TZS, UAH, UGX, USD, UYU, UZS, VEF, VND, VUV, WST, XAF, XAG, * XAU, XCD, XDR, XOF, XPD, XPF, XPT, YER, ZAR, ZMK, ZMW and ZWL. Any other provided currency symbol will result in a logged * warning and no other action taken by the SDK. * @param price Prices should be reported as NSDecimalNumber objects. Base units are treated the same as with SKProduct * from StoreKit and depend on the currency. As an example, USD should be reported as Dollars.Cents, whereas JPY should * be reported as a whole number of Yen. All provided NSDecimalNumber values will have NSRoundPlain rounding applied * such that a maximum of two digits exist after their decimal point. * @param quantity An unsigned number to indicate the purchase quantity. This number must be greater than 0 but no larger than 100. * @param properties An NSDictionary of properties to associate with this purchase. Property keys are non-empty NSString objects with * <= 255 characters and no leading dollar signs. Property values can be NSNumber integers, floats, booleans < 62 bits in length, NSDate objects or * NSString objects with <= 255 characters. * * @discussion Logs a purchase made in the application. * * Note: Braze supports purchases in multiple currencies. Purchases that you report in a currency other than USD will * be shown in the dashboard in USD based on the exchange rate at the date they were reported. */ - (void)logPurchase:(NSString *)productIdentifier inCurrency:(NSString *)currencyCode atPrice:(NSDecimalNumber *)price withQuantity:(NSUInteger)quantity andProperties:(nullable NSDictionary *)properties; /*! * If you're displaying cards on your own instead of using ABKFeedViewController, you should still report impressions of * the news feed back to Braze with this method so that your campaign reporting features still work in the dashboard. */ - (void)logFeedDisplayed; /*! * If you're displaying content cards on your own instead of using ABKContentCardsViewController, you should still report * impressions of the content cards back to Braze with this method so that your campaign reporting features still work * in the dashboard. */ - (void)logContentCardsDisplayed; /*! * Enqueues a news feed request for the current user. Note that if the queue already contains another request for the * current user, that the new feed request will be merged into the already existing request and only one will execute * for that user. * * When the new cards for news feed return from Braze server, the SDK will post an ABKFeedUpdatedNotification with an * ABKFeedUpdatedIsSuccessfulKey in the notification's userInfo dictionary to indicate if the news feed request is successful * or not. For more detail about the ABKFeedUpdatedNotification and the ABKFeedUpdatedIsSuccessfulKey, please check ABKFeedController. */ - (void)requestFeedRefresh; /*! * Enqueues a content cards request for the current user. */ - (void)requestContentCardsRefresh; /*! * Manually request geofences with a specific location. */ - (void)requestGeofencesWithLongitude:(double)longitude latitude:(double)latitude; /*! * Get the device ID - the IDFV - which will reset if all apps for a given vendor are removed from the device. * * @return The device ID. */ - (NSString *)getDeviceId; #if !TARGET_OS_TV /*! * @param deviceToken The device's push token. * * @discussion This method posts a token to Braze servers to associate the token with the current device. */ - (void)registerDeviceToken:(NSData *)deviceToken; /*! * @param application The app's UIApplication object * @param notification An NSDictionary passed in from the didReceiveRemoteNotification call * * @discussion This method forwards remote notifications to Braze. Call it from the application:didReceiveRemoteNotification * method of your App Delegate. */ - (void)registerApplication:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification NS_DEPRECATED_IOS(3_0, 10_0, "`registerApplication:didReceiveRemoteNotification:` is deprecated in iOS 10, please use `registerApplication:didReceiveRemoteNotification:fetchCompletionHandler:` instead."); /*! * @param application The app's UIApplication object * @param notification An NSDictionary passed in from the didReceiveRemoteNotification:fetchCompletionHandler: call * @param completionHandler A block passed in from the didReceiveRemoteNotification:fetchCompletionHandler: call * * @discussion This method forwards remote notifications to Braze. If the completionHandler is passed in when * the method is called, Braze will call the completionHandler. However, if the completionHandler is not passed in, * it is the host app's responsibility to call the completionHandler. * Call it from the application:didReceiveRemoteNotification:fetchCompletionHandler: method of your App Delegate. */ - (void)registerApplication:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification fetchCompletionHandler:(nullable void (^)(UIBackgroundFetchResult))completionHandler; /*! * @param identifier The action identifier passed in from the handleActionWithIdentifier:forRemoteNotification:. * @param userInfo An NSDictionary passed in from the handleActionWithIdentifier:forRemoteNotification: call. * @param completionHandler A block passed in from the didReceiveRemoteNotification:fetchCompletionHandler: call * * @discussion This method forwards remote notifications and the custom action chosen by user to Braze. Call it from * the application:handleActionWithIdentifier:forRemoteNotification: method of your App Delegate. */ - (void)getActionWithIdentifier:(NSString *)identifier forRemoteNotification:(NSDictionary *)userInfo completionHandler:(nullable void (^)(void))completionHandler NS_DEPRECATED_IOS(8_0, 10_0,"`getActionWithIdentifier:forRemoteNotification:completionHandler:` is deprecated in iOS 10, please use `userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:` instead."); /*! * @param center The app's current UNUserNotificationCenter object * @param response The UNNotificationResponse object passed in from the didReceiveNotificationResponse:withCompletionHandler: call * @param completionHandler A block passed in from the didReceiveNotificationResponse:withCompletionHandler: call. Braze will call * it at the end of the method if one is passed in. If you prefer to handle the completionHandler youself, please pass nil to Braze. * * @discussion This method forwards the response of the notification to Braze after user interacted with the notification. * Call it from the userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler: method of your App Delegate. */ - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(nullable void (^)(void))completionHandler NS_AVAILABLE_IOS(10_0); /*! * @param pushAuthGranted The boolean value passed in from completionHandler in UNUserNotificationCenter's * requestAuthorizationWithOptions:completionHandler: method, which indicates if the push authorization * was granted or not. * * @discussion This method forwards the push authorization result to Braze after the user interacts with * the notification prompt. * Call it from the UNUserNotificationCenter's requestAuthorizationWithOptions:completionHandler: method * when you prompt users to enable push. */ - (void)pushAuthorizationFromUserNotificationCenter:(BOOL)pushAuthGranted; #endif /*! * Adds SDK Metadata values to those automatically collected by the SDK. * * Metadata tell Braze how the SDK is integrated (e.g. wrapper, package manager, etc.) * * @param metadata The metadata values reflecting the current SDK integration. */ - (void)addSdkMetadata:(NSArray *)metadata; /* ------------------------------------------------------------------------------------------------------ * Data processing configuration methods. */ /*! * @discussion This method immediately wipes all data from the Braze iOS SDK. After this method is * called, the sharedInstance singleton will be nulled out and Braze functionality will be disabled * until the next call to startWithApiKey: in a subsequent app run. All references to the previous * singleton should be released. * * Note that the next call to startWithApiKey: must take place in a subsequent app run. Initializing the SDK * within the same app run after calling this method is not supported. * * The SDK will automatically re-enable itself when startWithApiKey: is called. There is * no need to call requestEnableSDKOnNextAppRun: to re-enable the SDK. wipeDataAndDisableForAppRun: * may be used at any time, including while the SDK is otherwise disabled. * * Note that if you are using unsafeInstance:, further calls to unsafeInstance: after using this * method will cause an uncaught exception to be thrown. We do not recommend using this method in * concert with unsafeInstance:. */ + (void)wipeDataAndDisableForAppRun; /*! * @discussion This method immediately disables the Braze iOS SDK. After this method is called, the * sharedInstance singleton will be nulled out and Braze functionality will be disabled until the * SDK is re-enabled via a call to requestEnableSDKOnNextAppRun: and re-initialized in a subsequent * app run via a call to startWithApiKey:. All references to the previous singleton should be released. * * Note that the next call to startWithApiKey: must take place in a subsequent app run. Initializing the SDK * within the same app run after calling this method is not supported. * * Unlike with wipeDataAndDisableForAppRun:, calling requestEnableSDKOnNextAppRun: is required to * re-enable the SDK after the method is called. * * Note that if you are using unsafeInstance:, further calls to unsafeInstance: after using this * method will cause an exception to be thrown. We do not recommend using this method in concert * with unsafeInstance:. */ + (void)disableSDK; /*! * @discussion This method requests the Braze iOS SDK to be re-enabled on the next app run. * After this method is called, the following call to startWithApiKey: will successfully * re-enable the SDK. Braze functionality will remain disabled until that point. * * Note that this method does not re-initialize the Appboy singleton on its own nor re-enable * Braze functionality immediately. */ + (void)requestEnableSDKOnNextAppRun; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyKit/include/AppboyKit.h ================================================ #import "Appboy.h" #import "ABKUser.h" #import "ABKFacebookUser.h" #import "ABKTwitterUser.h" #import "ABKAttributionData.h" // Cards #import "ABKCard.h" #import "ABKBannerCard.h" #import "ABKCaptionedImageCard.h" #import "ABKClassicCard.h" #import "ABKTextAnnouncementCard.h" // Content Card #import "ABKContentCard.h" #import "ABKBannerContentCard.h" #import "ABKCaptionedImageContentCard.h" #import "ABKClassicContentCard.h" // SDK Authentication #import "ABKSdkAuthenticationError.h" #import "ABKSdkAuthenticationDelegate.h" #if !TARGET_OS_TV // In-app Message #import "ABKInAppMessage.h" #import "ABKInAppMessageSlideup.h" #import "ABKInAppMessageImmersive.h" #import "ABKInAppMessageModal.h" #import "ABKInAppMessageFull.h" #import "ABKInAppMessageHTML.h" #import "ABKInAppMessageHTMLFull.h" #import "ABKInAppMessageHTMLBase.h" #import "ABKInAppMessageControl.h" #import "ABKInAppMessageControllerDelegate.h" #import "ABKInAppMessageController.h" #import "ABKInAppMessageButton.h" #import "ABKInAppMessageWebViewBridge.h" #import "ABKInAppMessageUIControlling.h" #import "ABKInAppMessageDarkTheme.h" #import "ABKInAppMessageDarkButtonTheme.h" // News Feed #import "ABKFeedController.h" // Content Cards Feed #import "ABKContentCardsController.h" // IDFA #import "ABKIDFADelegate.h" // SDWebImage #import "ABKSDWebImageProxy.h" // ABKImageDelegate #import "ABKImageDelegate.h" // Location #import "ABKLocationManager.h" #import "ABKLocationManagerProvider.h" #import "ABKURLDelegate.h" #import "ABKPushUtils.h" #import "ABKModalWebViewController.h" #import "ABKNoConnectionLocalization.h" #endif ================================================ FILE: AppboyPushStory/Dummy.m ================================================ ================================================ FILE: AppboyPushStory/include/AppboyPushStory/ABKStoriesView.h ================================================ // This class is an alternative version of iCarousel(https://github.com/nicklockwood/iCarousel). #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wreserved-id-macro" #pragma clang diagnostic ignored "-Wobjc-missing-property-synthesis" #import #import #import typedef NS_ENUM(NSInteger, ABKStoriesType) { CoverFlow = 0, Rotary, InvertedRotary, Cylinder, InvertedCylinder, Wheel, InvertedWheel, Linear, TimeMachine, InvertedTimeMachine }; NS_ASSUME_NONNULL_BEGIN @protocol ABKStoriesDataSource; @interface ABKStoriesView : UIView @property (nonatomic, weak) __nullable id dataSource; @property (nonatomic, assign) ABKStoriesType type; @property (nonatomic, assign) NSInteger currentPageIndex; @property (nonatomic, assign) CGFloat pageWidth; - (void)scrollToPageAtIndex:(NSInteger)index animated:(BOOL)animated; - (void)reloadData; @end @protocol ABKStoriesDataSource - (NSInteger)numberOfPagesInStoriesView:(ABKStoriesView *)storiesView; - (UIView *)storyView:(nullable UIView *)view atIndex:(NSInteger)index inStoriesView:(ABKStoriesView *)storiesView; @end NS_ASSUME_NONNULL_END #pragma clang diagnostic pop ================================================ FILE: AppboyPushStory/include/AppboyPushStory/ABKStoriesViewDataSource.h ================================================ #import #import #import #import "ABKStoriesView.h" @interface ABKStoriesViewDataSource : NSObject @property (nonatomic) NSMutableArray *images; @property (nonatomic) ABKStoriesView *storiesView; @property (nonatomic) NSArray *storyPages; @property (nonatomic) NSString *appGroup; - (instancetype)initWithNotification:(UNNotification *)notification storiesView:(ABKStoriesView *)storiesView appGroup:(NSString *)appGroup API_AVAILABLE(ios(10.0), macCatalyst(14.0)); - (UNNotificationContentExtensionResponseOption)didReceiveNotificationResponse:(UNNotificationResponse *)response API_AVAILABLE(ios(10.0), macCatalyst(14.0)); - (void)viewWillDisappear; @end ================================================ FILE: AppboyPushStory/include/AppboyPushStory/AppboyPushStory.h ================================================ #import FOUNDATION_EXPORT double AppboyPushStoryFrameworkVersionNumber; FOUNDATION_EXPORT const unsigned char AppboyPushStoryFrameworkVersionString[]; #import "ABKStoriesView.h" #import "ABKStoriesViewDataSource.h" ================================================ FILE: AppboyUI/ABKContentCards/AppboyContentCards.h ================================================ // Braze Content Cards View Controllers #import "ABKContentCardsViewController.h" #import "ABKContentCardsTableViewController.h" // Braze Content Cards Cells #import "ABKBannerContentCardCell.h" #import "ABKBaseContentCardCell.h" #import "ABKCaptionedImageContentCardCell.h" #import "ABKClassicContentCardCell.h" ================================================ FILE: AppboyUI/ABKContentCards/Resources/Base.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Done"; "Appboy.content-cards.no-card.text" = "We have no updates.\nPlease check again later."; "Appboy.content-cards.no-connection.title" = "Connection Error"; "Appboy.content-cards.no-connection.message" = "Cannot establish network connection.\nPlease try again later."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/ar.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "تم"; "Appboy.content-cards.no-card.text" = "ليس لدينا أي تحديث\n يرجى التحقق مرة أخرى لاحقاً"; "Appboy.content-cards.no-connection.title" = "خلل في الاتصال"; "Appboy.content-cards.no-connection.message" = "لا يمكن إجراء الاتصال بالشبكة\n يرجى تكرار المحاولة لاحقا "; ================================================ FILE: AppboyUI/ABKContentCards/Resources/cs.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Hotovo"; "Appboy.content-cards.no-card.text" = "Nemáme žádné aktualizace.\nZkontrolujte prosím znovu později."; "Appboy.content-cards.no-connection.title" = "Chyba připojení"; "Appboy.content-cards.no-connection.message" = "Nelze navázat síťové připojení.\nProsím zkuste to znovu později."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/da.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Afsluttet"; "Appboy.content-cards.no-card.text" = "Vi har ingen updates.\nPrøv venligst senere"; "Appboy.content-cards.no-connection.title" = "Netværksfejl"; "Appboy.content-cards.no-connection.message" = "Kan ikke etablere netværksforbindelse.\nPrøv venligst senere."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/de.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Fertig"; "Appboy.content-cards.no-card.text" = "Derzeit sind keine Updates verfügbar.\nBitte später noch einmal versuchen."; "Appboy.content-cards.no-connection.title" = "Verbindungsfehler"; "Appboy.content-cards.no-connection.message" = "Netzwerkverbindung kann nicht aufgebaut werden.\nBitte später noch einmal versuchen."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/en.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Done"; "Appboy.content-cards.no-card.text" = "We have no updates.\nPlease check again later."; "Appboy.content-cards.no-connection.title" = "Connection Error"; "Appboy.content-cards.no-connection.message" = "Cannot establish network connection.\nPlease try again later."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/es-419.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Listo"; "Appboy.content-cards.no-card.text" = "No tenemos ninguna actualización.\nVuelva a verificar más tarde."; "Appboy.content-cards.no-connection.title" = "Error de conexión"; "Appboy.content-cards.no-connection.message" = "No se puede establecer conexión con la red.\nPor favor, vuelva a intentarlo más tarde."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/es-MX.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Listo"; "Appboy.content-cards.no-card.text" = "No tenemos ninguna actualización.\nVuelva a verificar más tarde."; "Appboy.content-cards.no-connection.title" = "Error de conexión"; "Appboy.content-cards.no-connection.message" = "No se puede establecer conexión con la red.\nPor favor, vuelva a intentarlo más tarde."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/es.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Finalizado"; "Appboy.content-cards.no-card.text" = "No tenemos actualizaciones.\nPor favor compruébelo más tarde."; "Appboy.content-cards.no-connection.title" = "Error de conexión"; "Appboy.content-cards.no-connection.message" = "No se puede establecer conexión de red.\nPor favor inténtelo más tarde."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/et.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Valmis"; "Appboy.content-cards.no-card.text" = "Uuendusi pole praegu saadaval.\nProovige hiljem uuesti."; "Appboy.content-cards.no-connection.title" = "Üheduse viga"; "Appboy.content-cards.no-connection.message" = "Võrguühenduse loomine ebaõnnestus.\nProovige hiljem uuesti."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/fi.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Valmis"; "Appboy.content-cards.no-card.text" = "Päivityksiä ei ole saatavilla.\nTarkista myöhemmin uudelleen."; "Appboy.content-cards.no-connection.title" = "Yhteysvirhe"; "Appboy.content-cards.no-connection.message" = "Verkkoyhteyttä ei voida luoda.\nYritä myöhemmin uudelleen."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/fil.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Gawa na"; "Appboy.content-cards.no-card.text" = "Wala kaming mga update.\nMangyaring suriin muli sa ibang pagkakataon."; "Appboy.content-cards.no-connection.title" = "May Error sa Koneksyon"; "Appboy.content-cards.no-connection.message" = "Hindi makapagtatag ng koneksyon sa network.\nMangyaring subukan muli mamaya."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/fr.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Fini"; "Appboy.content-cards.no-card.text" = "Aucune mise à jour disponible.\nVeuillez vérifier ultérieurement."; "Appboy.content-cards.no-connection.title" = "Erreur de connexion."; "Appboy.content-cards.no-connection.message" = "Impossible d'établir la connexion réseau.\nVeuillez réessayer ultérieurement."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/he.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "סיום"; "Appboy.content-cards.no-card.text" = "אין לנו עדכונים\nבבקשה בדוק שוב בקרוב"; "Appboy.content-cards.no-connection.title" = "שגיאת חיבור רשת"; "Appboy.content-cards.no-connection.message" = "לא ניתן לקבוע חיבור רשת\nבבקשה נסה שוב בקרוב"; ================================================ FILE: AppboyUI/ABKContentCards/Resources/hi.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "कर दिया गया"; "Appboy.content-cards.no-card.text" = "हमारे पास कोई अपडेट नहीं हैं। कृपया बाद में फिर से जाँच करें.।"; "Appboy.content-cards.no-connection.title" = "कनेक्शन की त्रुटि"; "Appboy.content-cards.no-connection.message" = "नेटवर्क कनेक्शन स्थापित नहीं हो रहा है। कृपया बाद में दोबारा प्रयास करें।."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/id.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Selesai"; "Appboy.content-cards.no-card.text" = "Kami tidak memiliki pembaruan.\nCoba lagi nanti."; "Appboy.content-cards.no-connection.title" = "Kesalahan Koneksi"; "Appboy.content-cards.no-connection.message" = "Tidak bisa melakukan koneksi jaringan.\nCoba lagi nanti."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/it.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Fatto"; "Appboy.content-cards.no-card.text" = "Non ci sono aggiornamenti.\nRicontrollare più tardi."; "Appboy.content-cards.no-connection.title" = "Errore di connessione"; "Appboy.content-cards.no-connection.message" = "Impossibile stabilire una connessione di rete.\nRiprovare più tardi."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/ja.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "完了"; "Appboy.content-cards.no-card.text" = "アップデートはありません。\n後でもう一度確認してください。"; "Appboy.content-cards.no-connection.title" = "接続エラー"; "Appboy.content-cards.no-connection.message" = "ネットワークに接続できません。\n後でもう一度試してください。"; ================================================ FILE: AppboyUI/ABKContentCards/Resources/km.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "បានសម្រេច"; "Appboy.content-cards.no-card.text" = "យើងមិនមានការធ្វើបច្ចុប្បន្នភាពទេ។ សូមពិនិត្យមើលម្តងទៀតនៅពេលក្រោយ."; "Appboy.content-cards.no-connection.title" = "កំហុសឆ្គងក្នុងការតភ្ជាប់"; "Appboy.content-cards.no-connection.message" = "មិនអាចបង្កើតបណ្តាញតភ្ជាប់បានទេ។ សូមព្យាយាមម្តងទៀតនៅពេលក្រោយ."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/ko.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "완료 "; "Appboy.content-cards.no-card.text" = "업데이트가 없습니다.\n다음에 다시 확인해 주십시오."; "Appboy.content-cards.no-connection.title" = "연결 오류"; "Appboy.content-cards.no-connection.message" = "네트워크 연결을 할 수 없습니다.\n나중에 다시 시도해 주십시오."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/lo.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "ສຳ​ເລັດ"; "Appboy.content-cards.no-card.text" = "ພວກ​ເຮົາ​ບໍ່​ມີ​ການ​ອັບ​ເດດ.\nກະ​ລຸ​ນາ​ລອງ​ໃໝ່​ພາຍ​ຫຼັງ."; "Appboy.content-cards.no-connection.title" = "ການ​ເຊື່ອມ​ຕໍ່​ຜິດ​ພາດ"; "Appboy.content-cards.no-connection.message" = "ບໍ່​ສາ​ມາດ​ຕັ້ງ​ການ​ເຊື່ອມ​ຕໍ່​ເຄືອ​ຂ່າຍ​ໄດ້.\nກະ​ລຸ​ນາ​ລອງ​ໃໝ່​ພາຍ​ຫຼັງ."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/ms.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Selesai"; "Appboy.content-cards.no-card.text" = "Tiada kemas kini.\nSila periksa kemudian."; "Appboy.content-cards.no-connection.title" = "Ralat Sambungan"; "Appboy.content-cards.no-connection.message" = "Tidak boleh membuat sambungan rangkaian.\nSila cuba kemudian."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/my.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "ျပီးျပီ"; "Appboy.content-cards.no-card.text" = "ကၽႊႏု္ပ္ တို႕တြင္ အသစ္တင္ျပရန္မရွိပါ။ ေက်းဇူးျပဳ၍ ေနာင္တြင္ ထပ္စစ္ပါ။ ."; "Appboy.content-cards.no-connection.title" = "ဆက္သြယ္ေရး အမွား"; "Appboy.content-cards.no-connection.message" = "ကြန္ယက္ဆက္သြယ္ျခင္း မျပဳလုပ္ႏိုင္ပါ။ ေက်းဇူးျပဳ၍ ထပ္မံၾကိဳးစားၾကည္႕ပါ။."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/nb.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Ferdig"; "Appboy.content-cards.no-card.text" = "Vi har ingen oppdateringer.\nVennligst sjekk igjen senere."; "Appboy.content-cards.no-connection.title" = "Tilkoblingsfeil"; "Appboy.content-cards.no-connection.message" = "Kan ikke etablere nettverkstilkobling.\nVennligst prøv igjen senere."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/nl.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Gereed"; "Appboy.content-cards.no-card.text" = "Er zijn geen updates.\nProbeer het later opnieuw."; "Appboy.content-cards.no-connection.title" = "Verbindingsfout"; "Appboy.content-cards.no-connection.message" = "Kan geen netwerkverbinding maken.\nProbeer het later opnieuw."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/pl.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Gotowe"; "Appboy.content-cards.no-card.text" = "Brak aktualizacji.\nProszę sprawdzić ponownie później."; "Appboy.content-cards.no-connection.title" = "Błąd połączenia"; "Appboy.content-cards.no-connection.message" = "Nie można ustanowić połączenia z siecią.\nProszę spróbować ponownie później."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/pt-PT.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Concluído"; "Appboy.content-cards.no-card.text" = "Não temos atualizações.\nPor favor, verifique mais tarde."; "Appboy.content-cards.no-connection.title" = "Erro de Ligação"; "Appboy.content-cards.no-connection.message" = "Não é possível estabelecer a ligação à rede.\nPor favor, tente mais tarde."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/pt.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Concluído"; "Appboy.content-cards.no-card.text" = "Não temos nenhuma atualização.\nVerifique novamente mais tarde."; "Appboy.content-cards.no-connection.title" = "Erro de conexão"; "Appboy.content-cards.no-connection.message" = "Não é possível estabelecer uma conexão de rede.\nTente novamente mais tarde."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/ru.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Готово"; "Appboy.content-cards.no-card.text" = "Обновления недоступны.\nПожалуйста, проверьте снова позже."; "Appboy.content-cards.no-connection.title" = "Ошибка подключения"; "Appboy.content-cards.no-connection.message" = "Невозможно установить сетевое подключение.\nПожалуйста, повторите попытку позже."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/sv.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Klar"; "Appboy.content-cards.no-card.text" = "Det finns inga uppdateringar.\nFörsök igen senare."; "Appboy.content-cards.no-connection.title" = "Anslutningsfel"; "Appboy.content-cards.no-connection.message" = "Det gick inte att skapa en nätverksanslutning.\nFörsök igen senare."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/th.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "เสร็จสิ้น"; "Appboy.content-cards.no-card.text" = "เราไม่มีการอัพเดต กรุณาตรวจสอบภายหลัง."; "Appboy.content-cards.no-connection.title" = "ผิดพลาดการเชื่อมต่อ"; "Appboy.content-cards.no-connection.message" = "ไม่สามารถสร้างการเชื่อมต่อเครือข่าย กรุณาลองใหม่ภายหลัง."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/uk.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Готово"; "Appboy.content-cards.no-card.text" = "Оновлення недоступні.\nБудь ласка, перевірте знову пізніше."; "Appboy.content-cards.no-connection.title" = "Помилка підключення"; "Appboy.content-cards.no-connection.message" = "неможливо встановити з'єднання з мережею.\nБудь ласка, спробуйте ще раз пізніше."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/vi.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "Hoàn tất"; "Appboy.content-cards.no-card.text" = "Chúng tôi không có cập nhật nào.\nVui lòng kiểm tra lại sau."; "Appboy.content-cards.no-connection.title" = "Lỗi Kết Nối"; "Appboy.content-cards.no-connection.message" = "Không thể thiết lập kết nối mạng.\nVui lòng thử lại sau."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/zh-HK.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "完成"; "Appboy.content-cards.no-card.text" = "暫時沒有更新.\n請稍候再試."; "Appboy.content-cards.no-connection.title" = "連線錯誤"; "Appboy.content-cards.no-connection.message" = "無法建立網路連線.\n請稍候再試."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/zh-Hans.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "完成"; "Appboy.content-cards.no-card.text" = "暂时没有更新.\n请稍后再试."; "Appboy.content-cards.no-connection.title" = "连接错误"; "Appboy.content-cards.no-connection.message" = "无法建立网络连接.\n请稍候再试."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/zh-Hant.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "完成"; "Appboy.content-cards.no-card.text" = "暫時沒有更新.\n請稍候再試."; "Appboy.content-cards.no-connection.title" = "連線錯誤"; "Appboy.content-cards.no-connection.message" = "無法建立網路連線.\n請稍候再試."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/zh-TW.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "完成"; "Appboy.content-cards.no-card.text" = "暫時沒有更新.\n請稍候再試."; "Appboy.content-cards.no-connection.title" = "連線錯誤"; "Appboy.content-cards.no-connection.message" = "無法建立網路連線.\n請稍候再試."; ================================================ FILE: AppboyUI/ABKContentCards/Resources/zh.lproj/AppboyContentCardsLocalizable.strings ================================================ /* Content Cards Context Labels */ "Appboy.content-cards.done-button.title" = "完成"; "Appboy.content-cards.no-card.text" = "暂时没有更新.\n请稍后再试."; "Appboy.content-cards.no-connection.title" = "连接错误"; "Appboy.content-cards.no-connection.message" = "无法建立网络连接.\n请稍候再试."; ================================================ FILE: AppboyUI/ABKContentCards/ViewControllers/ABKContentCardsTableViewController.h ================================================ #import #import "AppboyKit.h" #import "ABKBaseContentCardCell.h" @protocol ABKContentCardsTableViewControllerDelegate; @interface ABKContentCardsTableViewController : UITableViewController /*! * UI elements which are used in the Content Cards table view. You can find them in the Content Cards Storyboard. */ @property (nonatomic, strong) IBOutlet UIView *emptyFeedView; @property (nonatomic, strong) IBOutlet UILabel *emptyFeedLabel; /*! * The ABKContentCardsTableViewController delegate */ @property (weak, nonatomic) id delegate; /*! * This property stores the cards displayed in the Content Cards feed. By default, the view controller * updates this value when it receives an ABKContentCardsProcessedNotification notification from the Braze SDK. * * This field's value should not be set directly from a subclass; instead, it should be set from within a populateContentCards: * implementation. */ @property (nonatomic) NSMutableArray *cards; /*! * This property allows you to enable or disable the unread indicator on the cards. The default * value is NO, which will enable the displaying of the unread indicator on cards. */ @property (assign, nonatomic) BOOL disableUnreadIndicator; /*! * This property defines the timeout for stored Content Cards in the Braze SDK. If the cards in the * Braze SDK are older than this value, the Content Cards view controller will request a Content Cards update. * * The default value is 60 seconds. */ @property NSTimeInterval cacheTimeout; /*! * If set, this property overrides the maximum width of Content Cards set by the storyboard. */ @property (assign, nonatomic) CGFloat maxContentCardWidth; /*! * This boolean determines if the Content Card will attempt to use dark theme colors, granted the device * is in dark mode. * * @discussion The default of this value is YES but can be overriden before the view controller is presented * to ensure that the dark theme is disabled for any Content Card displayed. */ @property (assign, nonatomic) BOOL enableDarkTheme; /*! * @discussion This method returns an instance of ABKContentCardsTableViewController. You can call it * to get a Content Cards view controller for your navigation controller. * @warning To use a custom Content Card view controller, instantiate your own subclass instead * (e.g. via alloc / init). */ + (instancetype)getNavigationContentCardsViewController; /*! * @discussion This method returns the localized string from AppboyContentCardsLocalizable.strings file. * You can easily override the localized string by adding the keys and the translations to your own * Localizable.strings file. * * To do custom handling with the Appboy localized string, you can override this method in a * subclass. */ - (NSString *)localizedAppboyContentCardsString:(NSString *)key; /*! * @discussion initialization that always occurs for the Content Cards table view controller */ - (void)setUp; /*! * @discussion Initialization that is in place of Storyboard or XIB initialization. * This method should call all the property specific setUp methods. */ - (void)setUpUI; /*! * @discussion specific view property initialization that is in place of Storyboard or XIB initialization. * Called by the setUpUI method and is exposed here to allow overriding. */ - (void)setUpEmptyFeedLabel; - (void)setUpEmptyFeedView; /*! * @discussion Registers Content Card type identifiers with the cell classes * that implement their view. */ - (void)registerTableViewCellClasses; /*! * @discussion Given a Content Card, return its type identifier */ - (NSString *)findCellIdentifierWithCard:(ABKContentCard *)card; /*! * @param tableView The table view which need the cell to diplay the card UI. * @param indexPath The index path of the card UI in the table view. * @param card The card model for the cell. * * @discussion This method dequeues and returns the corresponding card cell based on card type from * the given table view. */ - (ABKBaseContentCardCell *)dequeueCellFromTableView:(UITableView *)tableView forIndexPath:(NSIndexPath *)indexPath forCard:(ABKContentCard *)card; /*! * @discussion This method handles the user's click on the card. * * If you wish to handle card clicks yourself, refer to ABKContentCardsTableViewControllerDelegate's * contentCardTableViewController:shouldHandleCardClick: method. * * @warning Overriding handleCardClick: yourself might prevent * ABKContentCardsTableViewControllerDelegate's contentCardTableViewController:shouldHandleCardClick: * and contentCardTableViewController:didHandleCardClick: from firing properly. * * If you decide to override this method, you must call [card logContentCardClicked] manually inside of your * new method to send the click event to the Braze server. */ - (void)handleCardClick:(ABKContentCard *)card; - (void)requestNewCardsIfTimeout; /*! * @discussion This method is called when the cards stored in the cards property should be refreshed. */ - (void)populateContentCards; @end @protocol ABKContentCardsTableViewControllerDelegate @optional /*! * Asks the delegate if the Braze SDK should handle the content card click action. * * @warning This method might not be called if you overrode handleCardClick: * * @param viewController The view controller displaying the content card. * @param url The content card's url. * @return YES to let the Braze SDK handle the click action, NO if you wish to handle the click action * yourself. */ - (BOOL)contentCardTableViewController:(ABKContentCardsTableViewController *)viewController shouldHandleCardClick:(NSURL *)url; /*! * Informs the delegate that the content card click action was handled by the Braze SDK. * * This method is not called if the delegate method `contentCardTableViewController:shouldHandleCardClick:` * returns NO. * * @warning This method might not be called if you overrode handleCardClick: * * @param viewController The view controller displaying the content card. * @param url The content card's url. */ - (void)contentCardTableViewController:(ABKContentCardsTableViewController *)viewController didHandleCardClick:(NSURL *)url; @end ================================================ FILE: AppboyUI/ABKContentCards/ViewControllers/ABKContentCardsTableViewController.m ================================================ #import "ABKContentCardsTableViewController.h" #import "ABKContentCardsWebViewController.h" #import "ABKContentCardsController.h" #import "ABKContentCard.h" #import "ABKBannerContentCardCell.h" #import "ABKCaptionedImageContentCardCell.h" #import "ABKClassicContentCardCell.h" #import "ABKClassicImageContentCardCell.h" #import "ABKControlTableViewCell.h" #import "ABKUIUtils.h" #import "ABKUIURLUtils.h" static double const ABKContentCardsCacheTimeout = 1 * 60; // 1 minute static CGFloat const ABKContentCardsCellEstimatedHeight = 400.0f; @interface ABKContentCardsTableViewController () /*! * This set stores the content cards IDs for which the impressions have been logged. */ @property (nonatomic) NSMutableSet *cardImpressions; /*! * This set stores IDs for the content cards that are unviewed and on the screen right now. */ @property (nonatomic) NSMutableSet *unviewedOnScreenCards; /*! * There is some initialization such as associating which cell class to use in the table view that * is the responsibility of the storyboard if one is provided. If no story board is used then * the code in viewDidLoad will handle it. We can tell based on which init method is used. */ @property (nonatomic) BOOL usesStoryboard; - (void)logCardImpressionIfNeeded:(ABKContentCard *)card; - (void)requestContentCardsRefresh; - (void)contentCardsUpdated:(NSNotification *)notification; @end @implementation ABKContentCardsTableViewController #pragma mark - Initialization - (instancetype)init { self = [super init]; if (self) { self.usesStoryboard = NO; [self setUp]; [self setUpUI]; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { self.usesStoryboard = YES; [self setUp]; } return self; } #pragma mark - SetUp - (void)setUp { _cacheTimeout = ABKContentCardsCacheTimeout; _cardImpressions = [NSMutableSet set]; _unviewedOnScreenCards = [NSMutableSet set]; _enableDarkTheme = YES; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contentCardsUpdated:) name:ABKContentCardsProcessedNotification object:nil]; } - (void)setUpUI { [self setUpEmptyFeedLabel]; [self setUpEmptyFeedView]; } - (void)setUpEmptyFeedLabel { self.emptyFeedLabel = [[UILabel alloc] init]; self.emptyFeedLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleBody weight:UIFontWeightRegular]; self.emptyFeedLabel.adjustsFontSizeToFitWidth = YES; self.emptyFeedLabel.adjustsFontForContentSizeCategory = YES; self.emptyFeedLabel.textAlignment = NSTextAlignmentCenter; self.emptyFeedLabel.numberOfLines = 0; self.emptyFeedLabel.translatesAutoresizingMaskIntoConstraints = NO; } - (void)setUpEmptyFeedView { self.emptyFeedView = [[UIView alloc] init]; self.emptyFeedView.backgroundColor = [UIColor clearColor]; [self.emptyFeedView addSubview:self.emptyFeedLabel]; self.edgesForExtendedLayout = UIRectEdgeNone; NSLayoutConstraint *centerXConstraint = [self.emptyFeedLabel.centerXAnchor constraintEqualToAnchor:self.emptyFeedView.centerXAnchor]; NSLayoutConstraint *centerYConstraint = [self.emptyFeedLabel.centerYAnchor constraintEqualToAnchor:self.emptyFeedView.centerYAnchor]; NSLayoutConstraint *leadingConstraint = [self.emptyFeedLabel.leadingAnchor constraintEqualToAnchor:self.emptyFeedView.layoutMarginsGuide.leadingAnchor]; NSLayoutConstraint *trailingConstraint = [self.emptyFeedLabel.trailingAnchor constraintEqualToAnchor:self.emptyFeedView.layoutMarginsGuide.trailingAnchor]; NSLayoutConstraint *topConstraint = [self.emptyFeedLabel.topAnchor constraintEqualToAnchor:self.emptyFeedView.layoutMarginsGuide.topAnchor]; NSLayoutConstraint *bottomConstraint = [self.emptyFeedLabel.bottomAnchor constraintEqualToAnchor:self.emptyFeedView.layoutMarginsGuide.bottomAnchor]; [NSLayoutConstraint activateConstraints:@[centerXConstraint, centerYConstraint, leadingConstraint, trailingConstraint, topConstraint, bottomConstraint]]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)registerTableViewCellClasses { [self.tableView registerClass:[ABKCaptionedImageContentCardCell class] forCellReuseIdentifier:@"ABKCaptionedImageContentCardCell"]; [self.tableView registerClass:[ABKBannerContentCardCell class] forCellReuseIdentifier:@"ABKBannerContentCardCell"]; [self.tableView registerClass:[ABKClassicContentCardCell class] forCellReuseIdentifier:@"ABKClassicCardCell"]; [self.tableView registerClass:[ABKControlTableViewCell class] forCellReuseIdentifier:@"ABKControlCardCell"]; [self.tableView registerClass:[ABKClassicImageContentCardCell class] forCellReuseIdentifier:@"ABKClassicImageCardCell"]; } # pragma mark - View Controller Life Cycle Methods - (void)viewDidLoad { [super viewDidLoad]; if (@available(iOS 13.0, *)) { if (self.enableDarkTheme) { // This value will respect the system UI style of dark or light mode self.overrideUserInterfaceStyle = UIUserInterfaceStyleUnspecified; } else { self.overrideUserInterfaceStyle = UIUserInterfaceStyleLight; } } if (!self.usesStoryboard) { self.emptyFeedLabel.text = [self localizedAppboyContentCardsString:@"Appboy.content-cards.no-card.text"]; self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; if (@available(iOS 13.0, *)) { self.view.backgroundColor = [UIColor systemGroupedBackgroundColor]; } else { self.view.backgroundColor = [UIColor groupTableViewBackgroundColor]; } [self registerTableViewCellClasses]; UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init]; [refreshControl addTarget:self action:@selector(refreshContentCards:) forControlEvents:UIControlEventValueChanged]; self.refreshControl = refreshControl; } } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self requestNewCardsIfTimeout]; [self updateAndDisplayCardsFromCache]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.tableView reloadData]; }); } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [[Appboy sharedInstance] logContentCardsDisplayed]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; } - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator { [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; [coordinator animateAlongsideTransition:nil completion:^(id _Nonnull context) { [self.tableView reloadData]; }]; } #pragma mark - Update And Display Cached Cards - (void)populateContentCards { self.cards = [NSMutableArray arrayWithArray:[Appboy.sharedInstance.contentCardsController getContentCards]]; } - (void)requestContentCardsRefresh { [Appboy.sharedInstance requestContentCardsRefresh]; } - (IBAction)refreshContentCards:(UIRefreshControl *)sender { // Remove visible cards from unviewedOnScreenCards NSArray *visibleIndexPaths = [self.tableView indexPathsForVisibleRows]; for (NSIndexPath *indexPath in visibleIndexPaths) { ABKContentCard *card = self.cards[indexPath.row]; [self.unviewedOnScreenCards removeObject:card.idString]; } [self requestContentCardsRefresh]; } - (void)requestNewCardsIfTimeout { NSTimeInterval passedTime = fabs(Appboy.sharedInstance.contentCardsController.lastUpdate.timeIntervalSinceNow); if (passedTime > self.cacheTimeout) { [self requestContentCardsRefresh]; } else { // timeout is not passed, so we don't send a request for new content cards [self.refreshControl endRefreshing]; } } - (void)contentCardsUpdated:(NSNotification *)notification { BOOL isSuccessful = [notification.userInfo[ABKContentCardsProcessedIsSuccessfulKey] boolValue]; if (isSuccessful) { [self updateAndDisplayCardsFromCache]; } [self.refreshControl endRefreshing]; } - (void)updateAndDisplayCardsFromCache { [self populateContentCards]; if (self.cards == nil || self.cards.count == 0) { [self hideTableViewAndShowViewInBackground:self.emptyFeedView]; } else { [self showTableViewAndHideBackgroundViews]; } [self.tableView reloadData]; } - (void)logCardImpressionIfNeeded:(ABKContentCard *)card { if ([self.cardImpressions containsObject:card.idString]) { // do nothing if we have already logged an impression return; } if (![card isControlCard]) { if (card.viewed == NO) { [self.unviewedOnScreenCards addObject:card.idString]; } } [card logContentCardImpression]; [self.cardImpressions addObject:card.idString]; } #pragma mark - Table view header view - (void)hideTableViewAndShowViewInBackground:(UIView *)view { view.hidden = NO; view.frame = self.view.bounds; [view layoutIfNeeded]; self.tableView.backgroundView = view; } - (void)showTableViewAndHideBackgroundViews { self.emptyFeedView.hidden = YES; self.tableView.backgroundView = nil; } #pragma mark - Configuration Update - (void)setDisableUnreadIndicator:(BOOL)disableUnreadIndicator { if (disableUnreadIndicator != _disableUnreadIndicator) { _disableUnreadIndicator = disableUnreadIndicator; [self updateAndDisplayCardsFromCache]; } } #pragma mark - Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.cards.count; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { if ([self.cards[indexPath.row] isControlCard]) { return 0; } return UITableViewAutomaticDimension; } // Overrides the storyboard to get accurate cell height estimates to prevent from having // the scrollView jump if a cell needs to resize itself - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(nonnull NSIndexPath *)indexPath { return ABKContentCardsCellEstimatedHeight; } - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { ABKContentCard *card = self.cards[indexPath.row]; BOOL cellVisible = [[tableView indexPathsForVisibleRows] containsObject:indexPath]; if (cellVisible) { [self logCardImpressionIfNeeded:card]; } } - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { // We mark a cell as read only if it's not visible already. // But this method might be called for visible cells too because of dynamic heights. BOOL cellIsVisible = [[tableView indexPathsForVisibleRows] containsObject:indexPath]; if (!cellIsVisible && indexPath.row < self.cards.count) { // indexPath.row is out of bounds if the card did end displaying due to its deletion ABKContentCard *card = self.cards[indexPath.row]; [self.unviewedOnScreenCards removeObject:card.idString]; } } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { ABKContentCard *card = self.cards[indexPath.row]; ABKBaseContentCardCell *cell = [self dequeueCellFromTableView:tableView forIndexPath:indexPath forCard:card]; if (self.maxContentCardWidth > 0.0) { cell.cardWidthConstraint.constant = self.maxContentCardWidth; } BOOL viewedSetting = card.viewed; if ([self.unviewedOnScreenCards containsObject:card.idString]) { card.viewed = NO; } cell.delegate = self; [cell applyCard:card]; card.viewed = viewedSetting; cell.hideUnreadIndicator = self.disableUnreadIndicator; return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { ABKContentCard *card = self.cards[indexPath.row]; [self handleCardClick:card]; // Remove card from unviewedOnScreenCards [self.unviewedOnScreenCards removeObject:card.idString]; // Hide unviewed indicator ABKBaseContentCardCell *cell = [tableView cellForRowAtIndexPath:indexPath]; cell.unviewedLineView.hidden = YES; } - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { ABKContentCard *card = self.cards[indexPath.row]; return card.dismissible; } - (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { return UITableViewCellEditingStyleDelete; } - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { ABKContentCard *card = self.cards[indexPath.row]; [card logContentCardDismissed]; [self.cards removeObjectAtIndex:indexPath.row]; [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; if (self.cards.count == 0) { [self hideTableViewAndShowViewInBackground:self.emptyFeedView]; } } } #pragma mark - Dequeue cells - (ABKBaseContentCardCell *)dequeueCellFromTableView:(UITableView *)tableView forIndexPath:(NSIndexPath *)indexPath forCard:(ABKContentCard *)card { return [tableView dequeueReusableCellWithIdentifier:[self findCellIdentifierWithCard:card] forIndexPath:indexPath]; } - (NSString *)findCellIdentifierWithCard:(ABKContentCard *)card { if ([card isControlCard]) { return @"ABKControlCardCell"; } if ([card isKindOfClass:[ABKBannerContentCard class]]) { return @"ABKBannerContentCardCell"; } else if ([card isKindOfClass:[ABKCaptionedImageContentCard class]]) { return @"ABKCaptionedImageContentCardCell"; } else if ([card isKindOfClass:[ABKClassicContentCard class]]) { NSString *imageURL = [((ABKClassicContentCard *)card) image]; if (imageURL.length > 0) { return @"ABKClassicImageCardCell"; } else { return @"ABKClassicCardCell"; } } return nil; } #pragma mark - Card Click Actions - (void)handleCardClick:(ABKContentCard *)card { // Log a card click only when the card has the url property with a valid url. if (card.urlString.length <= 0) { return; } [card logContentCardClicked]; NSURL *cardURL = [ABKUIURLUtils getEncodedURIFromString:card.urlString]; // Content Cards Delegate handles card click action if ([self.delegate respondsToSelector:@selector(contentCardTableViewController:shouldHandleCardClick:)] && ![self.delegate contentCardTableViewController:self shouldHandleCardClick:cardURL]) { return; } // URL Delegate if ([ABKUIURLUtils URLDelegate:Appboy.sharedInstance.appboyUrlDelegate handlesURL:cardURL fromChannel:ABKContentCardChannel withExtras:nil]) { return; } // WebView if ([ABKUIURLUtils URL:cardURL shouldOpenInWebView:card.openUrlInWebView]) { [self openURLInWebView:cardURL]; } else { // System [ABKUIURLUtils openURLWithSystem:cardURL]; } // Delegate inform card click action if ([self.delegate respondsToSelector:@selector(contentCardTableViewController:didHandleCardClick:)]) { [self.delegate contentCardTableViewController:self didHandleCardClick:cardURL]; } } - (void)openURLInWebView:(NSURL *)url { ABKContentCardsWebViewController *webVC = [ABKContentCardsWebViewController new]; webVC.url = url; webVC.showDoneButton = (self.navigationItem.rightBarButtonItem != nil); [self.navigationController pushViewController:webVC animated:YES]; } #pragma mark - Utility Methods + (instancetype)getNavigationContentCardsViewController { return [[ABKContentCardsTableViewController alloc] init]; } - (NSString *)localizedAppboyContentCardsString:(NSString *)key { return [ABKUIUtils getLocalizedString:key inAppboyBundle:[ABKUIUtils bundle:[ABKContentCardsTableViewController class] channel:ABKContentCardChannel] table:@"AppboyContentCardsLocalizable"]; } #pragma mark - ABKBaseContentCardCellDelegate - (void)cellRequestSizeUpdate:(UITableViewCell *)cell { NSIndexPath *indexPath = [self.tableView indexPathForCell:cell]; if (indexPath == nil) { return; } [UIView performWithoutAnimation:^{ [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; }]; } @end ================================================ FILE: AppboyUI/ABKContentCards/ViewControllers/ABKContentCardsViewController.h ================================================ #import #import "ABKContentCardsTableViewController.h" @interface ABKContentCardsViewController : UINavigationController /*! * This property is the table view controller which displays all the content cards. It's also the root view * controller. */ @property (strong, nonatomic) ABKContentCardsTableViewController *contentCardsViewController; @end ================================================ FILE: AppboyUI/ABKContentCards/ViewControllers/ABKContentCardsViewController.m ================================================ #import "ABKContentCardsViewController.h" #import "ABKUIUtils.h" @implementation ABKContentCardsViewController - (instancetype)init { self = [super initWithRootViewController:[[ABKContentCardsTableViewController alloc] init]]; if (self) { self.contentCardsViewController = self.viewControllers.firstObject; [self addDoneButton]; #if !TARGET_OS_TV if (@available(iOS 15.0, *)) { self.view.backgroundColor = UIColor.systemGroupedBackgroundColor; } #endif } return self; } - (void)addDoneButton { UIBarButtonItem *closeBarButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(dismissContentCardsViewController:)]; [self.contentCardsViewController.navigationItem setRightBarButtonItem:closeBarButton]; } - (IBAction)dismissContentCardsViewController:(id)sender { [self dismissViewControllerAnimated:YES completion:nil]; } @end ================================================ FILE: AppboyUI/ABKContentCards/ViewControllers/ABKContentCardsWebViewController.h ================================================ #import #import @interface ABKContentCardsWebViewController : UIViewController /*! * The URL the modal web view controller should open. Please note that this is the initial URL and * won't be updated if the initial URL re-directs to another URL. */ @property NSURL *url; /*! * The WKWebView which displays the web page. */ @property (nonatomic) IBOutlet WKWebView *webView; /*! * The UIProgressView which shows the web view loading process. It will be on top of the web view and * will disappear as soon as the page is loaded. */ @property (nonatomic) IBOutlet UIProgressView *progressBar; /*! * The property tells the web view controller to add a Done button or not. The default value is NO. * Please set this property before displaying the web view controller. */ @property (nonatomic) BOOL showDoneButton; @end ================================================ FILE: AppboyUI/ABKContentCards/ViewControllers/ABKContentCardsWebViewController.m ================================================ #import "ABKContentCardsWebViewController.h" #import "ABKNoConnectionLocalization.h" #import "ABKUIUtils.h" static NSString *const EstimatedProgressKeyPath = @"estimatedProgress"; static NSString *const LocalizedNoConnectionKey = @"Appboy.no-connection.message"; @implementation ABKContentCardsWebViewController - (void)viewDidLoad { [super viewDidLoad]; self.webView.navigationDelegate = self; self.webView = [self getWebView]; self.view = self.webView; #if !TARGET_OS_TV if (@available(iOS 15.0, *)) { self.view.backgroundColor = UIColor.systemGroupedBackgroundColor; } #endif [self setupProgressBar]; if (self.showDoneButton) { UIBarButtonItem *closeBarButton = [self getDoneBarButtonItem]; [self.navigationItem setRightBarButtonItem:closeBarButton]; } [self.webView addObserver:self forKeyPath:EstimatedProgressKeyPath options:NSKeyValueObservingOptionNew context:nil]; [self.webView loadRequest:[NSURLRequest requestWithURL:self.url]]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([ABKUIUtils string:EstimatedProgressKeyPath isEqualToString:keyPath]) { if (self.webView.estimatedProgress == 1.0) { [UIView animateWithDuration:1 animations:^{ self.progressBar.alpha = 0.0; }]; } else if (self.webView.estimatedProgress < 1.0) { self.progressBar.alpha = 1.0; [self.progressBar setProgress:self.webView.estimatedProgress animated:YES]; } } } - (void)dealloc { [self.webView removeObserver:self forKeyPath:EstimatedProgressKeyPath]; } #pragma mark - Customization Methods /*! * @discussion Returns a WKWebView object, whose navigationDelegate is this ABKContentCardsWebViewController instance. * * If you want to do any customization to the WKWebView, please override this method in an ABKContentCardsWebViewController * category and return the customized WKWebView. All instances of ABKContentCardsWebViewController will then * call the category's `getWebView` implementation instead of this method. * */ - (WKWebView *)getWebView { WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero]; webView.navigationDelegate = self; return webView; } /*! * * @discussion Creates a UIProgressView and puts it on top of the web view. * * If you want to do any customization to the progress bar, please override this method in an ABKContentCardsWebViewController * category and set up the progress bar. All instances of ABKContentCardsWebViewController will then * call the category's `setupProgressBar:` implementation instead of this method. * */ - (void)setupProgressBar{ UIProgressView *progressBar = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleBar]; progressBar.alpha = 0; self.progressBar = progressBar; [self.view addSubview:self.progressBar]; self.progressBar.translatesAutoresizingMaskIntoConstraints = NO; [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.progressBar attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.topLayoutGuide attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[progressBar]|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:@{@"progressBar" : self.progressBar}]]; } /*! * @discussion Returns the Done UIBarButtonItem, which allows the user to dismiss the modal web view. * * If you want to do any customization to the Done button, please override this method in an ABKContentCardsWebViewController * category and return the customized UIBarButtonItem. All instances of ABKContentCardsWebViewController will then * call the category's `getDoneBarButtonItem` implementation instead of this method. * */ - (UIBarButtonItem *)getDoneBarButtonItem { return [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(closeButtonPressed:)]; } - (void)closeButtonPressed:(id)sender { [self dismissViewControllerAnimated:YES completion:nil]; } #pragma mark - WKNavigationDelegate methods - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { NSString *urlString = [[navigationAction.request.mainDocumentURL absoluteString] lowercaseString]; NSArray *stringComponents = [urlString componentsSeparatedByString:@":"]; if ([stringComponents[1] hasPrefix:@"//itunes.apple.com"] || (![stringComponents[0] isEqual:@"http"] && ![stringComponents[0] isEqual:@"https"])) { // Dismiss the modal web view and let the system handle the deep links if ([[UIApplication sharedApplication] openURL:navigationAction.request.URL]) { decisionHandler(WKNavigationActionPolicyCancel); [self.navigationController popViewControllerAnimated:NO]; return; } } decisionHandler(WKNavigationActionPolicyAllow); } - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { self.progressBar.alpha = 0.0; } - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error { self.progressBar.alpha = 0.0; UILabel *label = [[UILabel alloc] init]; label.textAlignment = NSTextAlignmentCenter; label.numberOfLines = 0; NSString *localizedNoConectionMessage = NSLocalizedString(@"Appboy.no-connection.message", @"No connection error message for URL loading failure"); if (localizedNoConectionMessage.length == 0 || [ABKUIUtils string:LocalizedNoConnectionKey isEqualToString:localizedNoConectionMessage]) { localizedNoConectionMessage = [ABKNoConnectionLocalization getNoConnectionLocalizedString]; } label.text = localizedNoConectionMessage; [self.webView addSubview:label]; label.translatesAutoresizingMaskIntoConstraints = NO; [self.webView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-10-[noConnectionLabel]-10-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:@{@"noConnectionLabel" : label}]]; [self.webView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[noConnectionLabel]|" options:NSLayoutFormatAlignAllCenterY metrics:nil views:@{@"noConnectionLabel" : label}]]; } @end ================================================ FILE: AppboyUI/ABKContentCards/ViewControllers/Cells/ABKBannerContentCardCell.h ================================================ #import "ABKBaseContentCardCell.h" #import "ABKBannerContentCard.h" @interface ABKBannerContentCardCell : ABKBaseContentCardCell @property (strong, nonatomic) IBOutlet UIImageView *bannerImageView; @property (strong, nonatomic) IBOutlet NSLayoutConstraint *imageRatioConstraint; - (void)applyCard:(ABKBannerContentCard *)bannerCard; @end ================================================ FILE: AppboyUI/ABKContentCards/ViewControllers/Cells/ABKBannerContentCardCell.m ================================================ #import "ABKBannerContentCardCell.h" #import "ABKBannerCard.h" #import "Appboy.h" #import "ABKImageDelegate.h" #import "ABKUIUtils.h" @implementation ABKBannerContentCardCell #pragma mark - Properties - (UIImageView *)bannerImageView { if (_bannerImageView != nil) { return _bannerImageView; } UIImageView *bannerImageView = [[[self imageViewClass] alloc] init]; bannerImageView.contentMode = UIViewContentModeScaleAspectFit; bannerImageView.translatesAutoresizingMaskIntoConstraints = NO; _bannerImageView = bannerImageView; return bannerImageView; } #pragma mark - SetUp - (void)setUpUI { [super setUpUI]; // Views [self.rootView addSubview:self.bannerImageView]; [self.rootView bringSubviewToFront:self.pinImageView]; [self.rootView bringSubviewToFront:self.unviewedLineView]; // AutoLayout self.imageRatioConstraint = [self.bannerImageView.heightAnchor constraintEqualToAnchor:self.bannerImageView.widthAnchor]; self.imageRatioConstraint.priority = UILayoutPriorityDefaultHigh; NSArray *constraints = @[ [self.bannerImageView.topAnchor constraintEqualToAnchor:self.rootView.topAnchor], [self.bannerImageView.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor], [self.bannerImageView.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor], [self.bannerImageView.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor], self.imageRatioConstraint ]; [NSLayoutConstraint activateConstraints:constraints]; } #pragma mark - ApplyCard - (void)applyCard:(ABKBannerContentCard *)card { if (![card isKindOfClass:[ABKBannerContentCard class]]) { return; } [super applyCard:card]; [self updateImageConstraintIfNeededWithAspectRatio:card.imageAspectRatio]; if (![Appboy sharedInstance].imageDelegate) { NSLog(@"[APPBOY][WARN] %@ %s", @"ABKImageDelegate on Appboy is nil. Image loading may be disabled.", __PRETTY_FUNCTION__); return; } typeof(self) __weak weakSelf = self; [[Appboy sharedInstance].imageDelegate setImageForView:self.bannerImageView showActivityIndicator:NO withURL:[NSURL URLWithString:card.image] imagePlaceHolder:[self getPlaceHolderImage] completed:^(UIImage * _Nullable image, NSError * _Nullable error, NSInteger cacheType, NSURL * _Nullable imageURL) { dispatch_async(dispatch_get_main_queue(), ^{ typeof(self) __strong strongSelf = weakSelf; if (strongSelf == nil) { return; } UIImage *finalImage = image != nil ? image : [strongSelf getPlaceHolderImage]; strongSelf.bannerImageView.image = finalImage; CGFloat aspectRatio = finalImage.size.width / finalImage.size.height; card.imageAspectRatio = aspectRatio; [strongSelf updateImageConstraintIfNeededWithAspectRatio:aspectRatio]; }); }]; } - (void)updateImageConstraintIfNeededWithAspectRatio:(CGFloat)aspectRatio { if (aspectRatio == 0 || ABK_CGFLT_EQ(self.imageRatioConstraint.multiplier, 1 / aspectRatio)) { return; } self.imageRatioConstraint.active = NO; self.imageRatioConstraint = [self.bannerImageView.heightAnchor constraintEqualToAnchor:self.bannerImageView.widthAnchor multiplier:1 / aspectRatio]; self.imageRatioConstraint.priority = UILayoutPriorityDefaultHigh; self.imageRatioConstraint.active = YES; [self.delegate cellRequestSizeUpdate:self]; } @end ================================================ FILE: AppboyUI/ABKContentCards/ViewControllers/Cells/ABKBaseContentCardCell.h ================================================ #import #import "ABKContentCard.h" @protocol ABKBaseContentCardCellDelegate - (void)cellRequestSizeUpdate:(UITableViewCell *)cell; @end @interface ABKBaseContentCardCell : UITableViewCell /*! * This view displays the card contents and is the base view container for each card. To change or * configure the outline of the card like card width, background color board width, etc, you can * update this property accordingly. */ @property (nonatomic) IBOutlet UIView *rootView; /*! * This is the triangle image which shows if a card has been viewed by the user. */ @property (nonatomic) IBOutlet UIImageView *pinImageView; /*! * This is the blue line under unviewed cards. */ @property (nonatomic) IBOutlet UIView *unviewedLineView; @property (nonatomic) UIColor *unviewedLineViewColor; /*! * Card root view related constraints */ @property (nonatomic) IBOutlet NSLayoutConstraint *rootViewLeadingConstraint; @property (nonatomic) IBOutlet NSLayoutConstraint *rootViewTrailingConstraint; @property (nonatomic) IBOutlet NSLayoutConstraint *rootViewTopConstraint; @property (nonatomic) IBOutlet NSLayoutConstraint *rootViewBottomConstraint; @property (nonatomic) IBOutlet NSLayoutConstraint *cardWidthConstraint; /*! * These are basic UI configuration for the Content Cards feed. They are set to the default values in the * `setUp` method. * * It's recommended to set the values before the view is displayed. */ @property (nonatomic, assign) CGFloat cardSidePadding; @property (nonatomic, assign) CGFloat cardSpacing; @property (nonatomic, assign) BOOL hideUnreadIndicator; /*! * To communicate back after any cell updates occur */ @property (weak, nonatomic) id delegate; /*! * @param card The card model for the cell. * * @discussion Apply the data from the given card to the card cell. */ - (void)applyCard:(ABKContentCard *)card; /*! * @discussion This is a utility method to return the place holder image. */ - (UIImage *)getPlaceHolderImage; - (Class)imageViewClass; /*! * @discussion initialization that always occurs for the content card cells */ - (void)setUp; /*! * @discussion Initialization that is in place of Storyboard or XIB initialization. * This method should call all the property specific setUp methods. */ - (void)setUpUI; /*! * @discussion This is a utility method to make text styled. */ - (void)applyAppboyAttributedTextStyleFrom:(NSString *)text forLabel:(UILabel *)label; @end static const UILayoutPriority ABKContentCardPriorityLayoutRequiredBelowAppleRequired = UILayoutPriorityRequired - 1; ================================================ FILE: AppboyUI/ABKContentCards/ViewControllers/Cells/ABKBaseContentCardCell.m ================================================ #import "ABKBaseContentCardCell.h" #import "ABKUIUtils.h" #import "Appboy.h" #import "ABKImageDelegate.h" static CGFloat AppboyCardSidePadding = 10.0; static CGFloat AppboyCardSpacing = 32.0; static CGFloat AppboyCardBorderWidth = 0.5; static CGFloat AppboyCardCornerRadius = 3.0; static CGFloat AppboyCardShadowXOffset = 0.0; static CGFloat AppboyCardShadowYOffset = -2.0; static CGFloat AppboyCardShadowOpacity = 0.5; static CGFloat AppboyCardLineSpacing = 1.2; @implementation ABKBaseContentCardCell #pragma mark - Properties - (UIView *)rootView { if (_rootView != nil) { return _rootView; } // View UIView *rootView = [[UIView alloc] init]; rootView.translatesAutoresizingMaskIntoConstraints = NO; if (@available(iOS 13.0, *)) { rootView.backgroundColor = [UIColor systemBackgroundColor]; } else { rootView.backgroundColor = [UIColor whiteColor]; } // - Border UIColor *lightBorderColor = [UIColor colorWithWhite:(224.0 / 255.0) alpha:1.0]; UIColor *darkBorderColor = [UIColor colorWithWhite:(85.0 / 255.0) alpha:1.0]; CALayer *rootLayer = rootView.layer; rootLayer.masksToBounds = YES; rootLayer.cornerRadius = AppboyCardCornerRadius; rootLayer.borderWidth = AppboyCardBorderWidth; rootLayer.borderColor = [ABKUIUtils dynamicColorForLightColor:lightBorderColor darkColor:darkBorderColor].CGColor; // - Shadow UIColor *shadowColor = [UIColor colorWithWhite:(178.0 / 255.0) alpha:1.0]; rootLayer.shadowColor = shadowColor.CGColor; rootLayer.shadowOffset = CGSizeMake(AppboyCardShadowXOffset, AppboyCardShadowYOffset); rootLayer.shadowOpacity = AppboyCardShadowOpacity; _rootView = rootView; return rootView; } - (UIImageView *)pinImageView { if (_pinImageView != nil) { return _pinImageView; } NSBundle *bundle = [ABKUIUtils bundle:[ABKBaseContentCardCell class] channel:ABKContentCardChannel]; UIImage *pinImage = [UIImage imageNamed:@"appboy_cc_icon_pinned" inBundle:bundle compatibleWithTraitCollection:nil]; pinImage = [pinImage imageFlippedForRightToLeftLayoutDirection]; UIImageView *pinImageView = [[UIImageView alloc] initWithImage:pinImage]; pinImageView.contentMode = UIViewContentModeScaleToFill; pinImageView.translatesAutoresizingMaskIntoConstraints = NO; _pinImageView = pinImageView; return pinImageView; } - (UIView *)unviewedLineView { if (_unviewedLineView != nil) { return _unviewedLineView; } UIView *unviewedLineView = [[UIView alloc] init]; unviewedLineView.backgroundColor = self.unviewedLineViewColor; unviewedLineView.translatesAutoresizingMaskIntoConstraints = NO; _unviewedLineView = unviewedLineView; return unviewedLineView; } #pragma mark - Initialization - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { [self setUp]; [self setUpUI]; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { if (self = [super initWithCoder:aDecoder]) { [self setUp]; } return self; } #pragma mark - SetUp - (void)setUp { self.backgroundColor = [UIColor clearColor]; self.contentView.backgroundColor = [UIColor clearColor]; self.selectionStyle = UITableViewCellSelectionStyleNone; self.unviewedLineViewColor = self.tintColor; self.cardSidePadding = AppboyCardSidePadding; self.cardSpacing = AppboyCardSpacing; } - (void)setUpUI { // View Hierarchy [self.contentView addSubview:self.rootView]; [self.rootView addSubview:self.pinImageView]; [self.rootView addSubview:self.unviewedLineView]; // AutoLayout // - Root self.rootViewLeadingConstraint = [self.rootView.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor constant:self.cardSidePadding]; self.rootViewTrailingConstraint = [self.rootView.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor constant:-self.cardSidePadding]; self.rootViewTopConstraint = [self.rootView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor constant:self.cardSidePadding]; self.rootViewBottomConstraint = [self.rootView.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor constant:-self.cardSidePadding]; self.cardWidthConstraint = [self.rootView.widthAnchor constraintLessThanOrEqualToConstant:380]; self.rootViewLeadingConstraint.priority = ABKContentCardPriorityLayoutRequiredBelowAppleRequired; self.rootViewTrailingConstraint.priority = ABKContentCardPriorityLayoutRequiredBelowAppleRequired; // - All constraints NSArray *constraints = @[ // Root view self.rootViewLeadingConstraint, self.rootViewTrailingConstraint, self.rootViewTopConstraint, self.rootViewBottomConstraint, self.cardWidthConstraint, [self.rootView.centerXAnchor constraintEqualToAnchor:self.contentView.centerXAnchor], // PinImage [self.pinImageView.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor], [self.pinImageView.topAnchor constraintEqualToAnchor:self.rootView.topAnchor], [self.pinImageView.widthAnchor constraintEqualToConstant:20], [self.pinImageView.heightAnchor constraintEqualToConstant:20], // UnviewedLine [self.unviewedLineView.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor], [self.unviewedLineView.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor], [self.unviewedLineView.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor], [self.unviewedLineView.heightAnchor constraintEqualToConstant:8] ]; [NSLayoutConstraint activateConstraints:constraints]; } # pragma mark - Cell UI Configuration - (void)setUnviewedLineViewColor:(UIColor*)bgColor { _unviewedLineViewColor = bgColor; if (self.unviewedLineView) { self.unviewedLineView.backgroundColor = self.unviewedLineViewColor; } } - (void)setHideUnreadIndicator:(BOOL)hideUnreadIndicator { if (_hideUnreadIndicator != hideUnreadIndicator) { _hideUnreadIndicator = hideUnreadIndicator; self.unviewedLineView.hidden = hideUnreadIndicator; } } - (void)setCardSidePadding:(CGFloat)sidePadding { _cardSidePadding = sidePadding; if (self.rootViewLeadingConstraint && self.rootViewTrailingConstraint) { self.rootViewLeadingConstraint.constant = self.cardSidePadding; self.rootViewTrailingConstraint.constant = self.cardSidePadding; } } - (void)setCardSpacing:(CGFloat)spacing { _cardSpacing = spacing; if (self.rootViewTopConstraint && self.rootViewBottomConstraint) { self.rootViewTopConstraint.constant = self.cardSpacing / 2.0; self.rootViewBottomConstraint.constant = self.cardSpacing / 2.0; } } #pragma mark - ApplyCard - (void)applyCard:(ABKContentCard *)card { if ([card isControlCard]) { self.pinImageView.hidden = YES; self.unviewedLineView.hidden = YES; return; } self.unviewedLineView.hidden = self.hideUnreadIndicator || card.viewed; self.pinImageView.hidden = !card.pinned; } #pragma mark - Utiliy Methods - (UIImage *)getPlaceHolderImage { return [ABKUIUtils imageNamed:@"appboy_cc_noimage_lrg" bundle:[ABKBaseContentCardCell class] channel:ABKContentCardChannel]; } - (Class)imageViewClass { if ([Appboy sharedInstance].imageDelegate) { return [[Appboy sharedInstance].imageDelegate imageViewClass]; } return [UIImageView class]; } - (void)applyAppboyAttributedTextStyleFrom:(NSString *)text forLabel:(UILabel *)label { UIColor *color = label.textColor; NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; paragraphStyle.lineSpacing = AppboyCardLineSpacing; UIFont *font = label.font; NSDictionary *attributes = @{NSFontAttributeName: font, NSForegroundColorAttributeName: color, NSParagraphStyleAttributeName: paragraphStyle}; // Convert to empty string to fail gracefully if given null from backend text = text ?: @""; label.attributedText = [[NSAttributedString alloc] initWithString:text attributes:attributes]; } @end ================================================ FILE: AppboyUI/ABKContentCards/ViewControllers/Cells/ABKCaptionedImageContentCardCell.h ================================================ #import "ABKBaseContentCardCell.h" #import "ABKCaptionedImageContentCard.h" @interface ABKCaptionedImageContentCardCell : ABKBaseContentCardCell @property (class, nonatomic) UIColor *titleLabelColor; @property (class, nonatomic) UIColor *descriptionLabelColor; @property (class, nonatomic) UIColor *linkLabelColor; @property (strong, nonatomic) IBOutlet UIImageView *captionedImageView; @property (strong, nonatomic) IBOutlet NSLayoutConstraint *imageRatioConstraint; @property (strong, nonatomic) IBOutlet UILabel *titleLabel; @property (strong, nonatomic) IBOutlet UILabel *descriptionLabel; @property (strong, nonatomic) IBOutlet UILabel *linkLabel; @property (nonatomic, assign) CGFloat padding; - (void)applyCard:(ABKCaptionedImageContentCard *)captionedImageCard; @end ================================================ FILE: AppboyUI/ABKContentCards/ViewControllers/Cells/ABKCaptionedImageContentCardCell.m ================================================ #import "ABKCaptionedImageContentCardCell.h" #import "Appboy.h" #import "ABKImageDelegate.h" #import "ABKUIUtils.h" @interface ABKCaptionedImageContentCardCell () @property (strong, nonatomic) NSArray *descriptionConstraints; @property (strong, nonatomic) NSArray *linkConstraints; @end @implementation ABKCaptionedImageContentCardCell static UIColor *_titleLabelColor = nil; static UIColor *_descriptionLabelColor = nil; static UIColor *_linkLabelColor = nil; + (UIColor *)titleLabelColor { if (_titleLabelColor == nil) { if (@available(iOS 13.0, *)) { _titleLabelColor = [UIColor labelColor]; } else { _titleLabelColor = [UIColor blackColor]; } } return _titleLabelColor; } + (void)setTitleLabelColor:(UIColor *)titleLabelColor { _titleLabelColor = titleLabelColor; } + (UIColor *)descriptionLabelColor { if (_descriptionLabelColor == nil) { if (@available(iOS 13.0, *)) { _descriptionLabelColor = [UIColor labelColor]; } else { _descriptionLabelColor = [UIColor blackColor]; } } return _descriptionLabelColor; } + (void)setDescriptionLabelColor:(UIColor *)descriptionLabelColor { _descriptionLabelColor = descriptionLabelColor; } + (UIColor *)linkLabelColor { if (_linkLabelColor == nil) { if (@available(iOS 13.0, *)) { _linkLabelColor = [UIColor linkColor]; } else { _linkLabelColor = [UIColor systemBlueColor]; } } return _linkLabelColor; } + (void)setLinkLabelColor:(UIColor *)linkLabelColor{ _linkLabelColor = linkLabelColor; } #pragma mark - Properties - (UIImageView *)captionedImageView { if (_captionedImageView != nil) { return _captionedImageView; } UIImageView *captionedImageView = [[[self imageViewClass] alloc] init]; captionedImageView.contentMode = UIViewContentModeScaleAspectFit; captionedImageView.translatesAutoresizingMaskIntoConstraints = NO; _captionedImageView = captionedImageView; return captionedImageView; } - (UILabel *)titleLabel { if (_titleLabel != nil) { return _titleLabel; } UILabel *titleLabel = [[UILabel alloc] init]; titleLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleCallout weight:UIFontWeightBold]; titleLabel.textColor = [self class].titleLabelColor; titleLabel.text = @"Title"; titleLabel.numberOfLines = 0; titleLabel.lineBreakMode = NSLineBreakByWordWrapping; titleLabel.translatesAutoresizingMaskIntoConstraints = NO; _titleLabel = titleLabel; return titleLabel; } - (UILabel *)descriptionLabel { if (_descriptionLabel != nil) { return _descriptionLabel; } UILabel *descriptionLabel = [[UILabel alloc] init]; descriptionLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleFootnote weight:UIFontWeightRegular]; descriptionLabel.textColor = [self class].descriptionLabelColor; descriptionLabel.text = @"Description"; descriptionLabel.numberOfLines = 0; descriptionLabel.translatesAutoresizingMaskIntoConstraints = NO; _descriptionLabel = descriptionLabel; return descriptionLabel; } - (UILabel *)linkLabel { if (_linkLabel != nil) { return _linkLabel; } UILabel *linkLabel = [[UILabel alloc] init]; linkLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleFootnote weight:UIFontWeightMedium]; linkLabel.textColor = [self class].linkLabelColor; linkLabel.text = @"Link"; linkLabel.numberOfLines = 0; linkLabel.lineBreakMode = NSLineBreakByCharWrapping; linkLabel.translatesAutoresizingMaskIntoConstraints = NO; _linkLabel = linkLabel; return linkLabel; } #pragma mark - SetUp - (void)setUp { [super setUp]; self.padding = 25; } - (void)setUpUI { [super setUpUI]; // Views [self.rootView addSubview:self.captionedImageView]; [self.rootView addSubview:self.titleLabel]; [self.rootView addSubview:self.descriptionLabel]; [self.rootView addSubview:self.linkLabel]; // - Remove / add pinImageView to reset it [self.pinImageView removeFromSuperview]; [self.rootView addSubview:self.pinImageView]; // AutoLayout self.imageRatioConstraint = [self.captionedImageView.heightAnchor constraintEqualToAnchor:self.captionedImageView.widthAnchor]; self.imageRatioConstraint.priority = UILayoutPriorityDefaultHigh; NSArray *constraints = @[ // Captioned Image [self.captionedImageView.topAnchor constraintEqualToAnchor:self.rootView.topAnchor], [self.captionedImageView.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor], [self.captionedImageView.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor], self.imageRatioConstraint, // Pin Image [self.pinImageView.topAnchor constraintEqualToAnchor:self.captionedImageView.topAnchor], [self.pinImageView.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor], [self.pinImageView.widthAnchor constraintEqualToConstant:20], [self.pinImageView.heightAnchor constraintEqualToConstant:20], // Title [self.titleLabel.topAnchor constraintEqualToAnchor:self.captionedImageView.bottomAnchor constant:17], [self.titleLabel.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor constant:self.padding], [self.titleLabel.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor constant:-self.padding], // Description [self.descriptionLabel.topAnchor constraintEqualToAnchor:self.titleLabel.bottomAnchor constant:6], [self.descriptionLabel.leadingAnchor constraintEqualToAnchor:self.titleLabel.leadingAnchor], [self.descriptionLabel.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor], // Link [self.linkLabel.leadingAnchor constraintEqualToAnchor:self.titleLabel.leadingAnchor], [self.linkLabel.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor] ]; [NSLayoutConstraint activateConstraints:constraints]; self.descriptionConstraints = @[ [self.descriptionLabel.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor constant:-self.padding] ]; self.linkConstraints = @[ [self.linkLabel.topAnchor constraintEqualToAnchor:self.descriptionLabel.bottomAnchor constant:8], [self.linkLabel.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor constant:-self.padding] ]; } #pragma mark - ApplyCard - (void)applyCard:(ABKCaptionedImageContentCard *)card { if (![card isKindOfClass:[ABKCaptionedImageContentCard class]]) { return; } [super applyCard:card]; [self applyAppboyAttributedTextStyleFrom:card.title forLabel:self.titleLabel]; [self applyAppboyAttributedTextStyleFrom:card.cardDescription forLabel:self.descriptionLabel]; [self applyAppboyAttributedTextStyleFrom:card.domain forLabel:self.linkLabel]; self.linkLabel.hidden = card.domain.length == 0; [self updateConstraintsForCard:card]; [self updateImageConstraintIfNeededWithAspectRatio:card.imageAspectRatio]; if (![Appboy sharedInstance].imageDelegate) { NSLog(@"[APPBOY][WARN] %@ %s", @"ABKImageDelegate on Appboy is nil. Image loading may be disabled.", __PRETTY_FUNCTION__); return; } typeof(self) __weak weakSelf = self; [[Appboy sharedInstance].imageDelegate setImageForView:self.captionedImageView showActivityIndicator:NO withURL:[NSURL URLWithString:card.image] imagePlaceHolder:[self getPlaceHolderImage] completed:^(UIImage * _Nullable image, NSError * _Nullable error, NSInteger cacheType, NSURL * _Nullable imageURL) { dispatch_async(dispatch_get_main_queue(), ^{ typeof(self) __strong strongSelf = weakSelf; if (strongSelf == nil) { return; } if (image == nil) { strongSelf.captionedImageView.image = [strongSelf getPlaceHolderImage]; return; } CGFloat aspectRatio = image.size.width / image.size.height; card.imageAspectRatio = aspectRatio; [strongSelf updateImageConstraintIfNeededWithAspectRatio:aspectRatio]; }); }]; } - (void)updateConstraintsForCard:(ABKCaptionedImageContentCard *)card { if (card.domain.length == 0) { [NSLayoutConstraint deactivateConstraints:self.linkConstraints]; [NSLayoutConstraint activateConstraints:self.descriptionConstraints]; } else { [NSLayoutConstraint deactivateConstraints:self.descriptionConstraints]; [NSLayoutConstraint activateConstraints:self.linkConstraints]; } } - (void)updateImageConstraintIfNeededWithAspectRatio:(CGFloat)aspectRatio { if (aspectRatio == 0 || ABK_CGFLT_EQ(self.imageRatioConstraint.multiplier, 1 / aspectRatio)) { return; } self.imageRatioConstraint.active = NO; self.imageRatioConstraint = [self.captionedImageView.heightAnchor constraintEqualToAnchor:self.captionedImageView.widthAnchor multiplier:1 / aspectRatio]; self.imageRatioConstraint.priority = UILayoutPriorityDefaultHigh; self.imageRatioConstraint.active = YES; [self.delegate cellRequestSizeUpdate:self]; } @end ================================================ FILE: AppboyUI/ABKContentCards/ViewControllers/Cells/ABKClassicContentCardCell.h ================================================ #import "ABKBaseContentCardCell.h" #import "ABKClassicContentCard.h" @interface ABKClassicContentCardCell : ABKBaseContentCardCell @property (class, nonatomic) UIColor *titleLabelColor; @property (class, nonatomic) UIColor *descriptionLabelColor; @property (class, nonatomic) UIColor *linkLabelColor; @property (strong, nonatomic) IBOutlet UILabel *titleLabel; @property (strong, nonatomic) IBOutlet UILabel *descriptionLabel; @property (strong, nonatomic) IBOutlet UILabel *linkLabel; @property (strong, nonatomic) NSArray *descriptionConstraints; @property (strong, nonatomic) NSArray *linkConstraints; @property (nonatomic, assign) CGFloat padding; - (void)applyCard:(ABKClassicContentCard *)classicCard; @end ================================================ FILE: AppboyUI/ABKContentCards/ViewControllers/Cells/ABKClassicContentCardCell.m ================================================ #import "ABKClassicContentCardCell.h" #import "ABKUIUtils.h" @implementation ABKClassicContentCardCell static UIColor *_titleLabelColor = nil; static UIColor *_descriptionLabelColor = nil; static UIColor *_linkLabelColor = nil; + (UIColor *)titleLabelColor { if (_titleLabelColor == nil) { if (@available(iOS 13.0, *)) { _titleLabelColor = [UIColor labelColor]; } else { _titleLabelColor = [UIColor blackColor]; } } return _titleLabelColor; } + (void)setTitleLabelColor:(UIColor *)titleLabelColor { _titleLabelColor = titleLabelColor; } + (UIColor *)descriptionLabelColor { if (_descriptionLabelColor == nil) { if (@available(iOS 13.0, *)) { _descriptionLabelColor = [UIColor labelColor]; } else { _descriptionLabelColor = [UIColor blackColor]; } } return _descriptionLabelColor; } + (void)setDescriptionLabelColor:(UIColor *)descriptionLabelColor { _descriptionLabelColor = descriptionLabelColor; } + (UIColor *)linkLabelColor { if (_linkLabelColor == nil) { if (@available(iOS 13.0, *)) { _linkLabelColor = [UIColor linkColor]; } else { _linkLabelColor = [UIColor systemBlueColor]; } } return _linkLabelColor; } + (void)setLinkLabelColor:(UIColor *)linkLabelColor{ _linkLabelColor = linkLabelColor; } #pragma mark - Properties - (UILabel *)titleLabel { if (_titleLabel != nil) { return _titleLabel; } UILabel *titleLabel = [[UILabel alloc] init]; titleLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleCallout weight:UIFontWeightBold]; titleLabel.textColor = [self class].titleLabelColor; titleLabel.text = @"Title"; titleLabel.numberOfLines = 0; titleLabel.lineBreakMode = NSLineBreakByWordWrapping; titleLabel.translatesAutoresizingMaskIntoConstraints = NO; _titleLabel = titleLabel; return titleLabel; } - (UILabel *)descriptionLabel { if (_descriptionLabel != nil) { return _descriptionLabel; } UILabel *descriptionLabel = [[UILabel alloc] init]; descriptionLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleFootnote weight:UIFontWeightRegular]; descriptionLabel.textColor = [self class].descriptionLabelColor; descriptionLabel.text = @"Description"; descriptionLabel.numberOfLines = 0; descriptionLabel.lineBreakMode = NSLineBreakByWordWrapping; descriptionLabel.translatesAutoresizingMaskIntoConstraints = NO; _descriptionLabel = descriptionLabel; return descriptionLabel; } - (UILabel *)linkLabel { if (_linkLabel != nil) { return _linkLabel; } UILabel *linkLabel = [[UILabel alloc] init]; linkLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleFootnote weight:UIFontWeightMedium]; linkLabel.textColor = [self class].linkLabelColor; linkLabel.text = @"Link"; linkLabel.numberOfLines = 0; linkLabel.lineBreakMode = NSLineBreakByCharWrapping; linkLabel.translatesAutoresizingMaskIntoConstraints = NO; _linkLabel = linkLabel; return linkLabel; } #pragma mark - SetUp - (void)setUp { [super setUp]; self.padding = 25; } - (void)setUpUI { [super setUpUI]; // Views [self.rootView addSubview:self.titleLabel]; [self.rootView addSubview:self.descriptionLabel]; [self.rootView addSubview:self.linkLabel]; NSLayoutConstraint *titleTrailingConstraint = [self.titleLabel.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor constant:-self.padding]; titleTrailingConstraint.priority = ABKContentCardPriorityLayoutRequiredBelowAppleRequired; // AutoLayout NSArray *constraints = @[ // Title // - Top [self.titleLabel.topAnchor constraintEqualToAnchor:self.rootView.topAnchor constant:17], // - Horizontal [self.titleLabel.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor constant:self.padding], titleTrailingConstraint, // Description // - Top [self.descriptionLabel.topAnchor constraintEqualToAnchor:self.titleLabel.bottomAnchor constant:6], // - Horizontal [self.descriptionLabel.leadingAnchor constraintEqualToAnchor:self.titleLabel.leadingAnchor], [self.descriptionLabel.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor], // Link // - Horizontal [self.linkLabel.leadingAnchor constraintEqualToAnchor:self.titleLabel.leadingAnchor], [self.linkLabel.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor] ]; [NSLayoutConstraint activateConstraints:constraints]; self.descriptionConstraints = @[ [self.descriptionLabel.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor constant:-self.padding] ]; self.linkConstraints = @[ [self.linkLabel.topAnchor constraintEqualToAnchor:self.descriptionLabel.bottomAnchor constant:8], [self.linkLabel.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor constant:-self.padding] ]; } #pragma mark - ApplyCard - (void)applyCard:(ABKClassicContentCard *)card { if (![card isKindOfClass:[ABKClassicContentCard class]]) { return; } [super applyCard:card]; [self applyAppboyAttributedTextStyleFrom:card.title forLabel:self.titleLabel]; [self applyAppboyAttributedTextStyleFrom:card.cardDescription forLabel:self.descriptionLabel]; [self applyAppboyAttributedTextStyleFrom:card.domain forLabel:self.linkLabel]; self.linkLabel.hidden = card.domain.length == 0; [self updateConstraintsForCard:card]; } - (void)updateConstraintsForCard:(ABKClassicContentCard *)card { if (card.domain.length == 0) { [NSLayoutConstraint deactivateConstraints:self.linkConstraints]; [NSLayoutConstraint activateConstraints:self.descriptionConstraints]; } else { [NSLayoutConstraint deactivateConstraints:self.descriptionConstraints]; [NSLayoutConstraint activateConstraints:self.linkConstraints]; } } @end ================================================ FILE: AppboyUI/ABKContentCards/ViewControllers/Cells/ABKClassicImageContentCardCell.h ================================================ #import "ABKClassicContentCardCell.h" /*! * The ABKClassicContentCard has an optional image property. * Use this view controller for a classic card with an image and ABKClassicContentCardCell for a * classic card without an image. */ @interface ABKClassicImageContentCardCell : ABKClassicContentCardCell @property (strong, nonatomic) IBOutlet UIImageView *classicImageView; - (void)applyCard:(ABKClassicContentCard *)classicCard; @end ================================================ FILE: AppboyUI/ABKContentCards/ViewControllers/Cells/ABKClassicImageContentCardCell.m ================================================ #import "ABKClassicImageContentCardCell.h" #import "Appboy.h" #import "ABKImageDelegate.h" #import "ABKUIUtils.h" @implementation ABKClassicImageContentCardCell #pragma mark - Properties - (UIImageView *)classicImageView { if (_classicImageView != nil) { return _classicImageView; } UIImageView *classicImageView = [[[self imageViewClass] alloc] init]; classicImageView.contentMode = UIViewContentModeScaleAspectFit; classicImageView.translatesAutoresizingMaskIntoConstraints = NO; classicImageView.clipsToBounds = YES; _classicImageView = classicImageView; return classicImageView; } #pragma mark - SetUp - (void)setUpUI { [super setUpUI]; // Reset [self.titleLabel removeFromSuperview]; [self.descriptionLabel removeFromSuperview]; [self.linkLabel removeFromSuperview]; // Views [self.rootView addSubview:self.classicImageView]; [self.rootView addSubview:self.titleLabel]; [self.rootView addSubview:self.descriptionLabel]; [self.rootView addSubview:self.linkLabel]; NSLayoutConstraint *titleTrailingConstraint = [self.titleLabel.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor constant:-self.padding]; titleTrailingConstraint.priority = ABKContentCardPriorityLayoutRequiredBelowAppleRequired; // AutoLayout NSArray *constraints = @[ // ClassicImage [self.classicImageView.topAnchor constraintEqualToAnchor:self.rootView.topAnchor constant:17], [self.classicImageView.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor constant:self.padding], [self.classicImageView.heightAnchor constraintEqualToConstant:57.5], [self.classicImageView.widthAnchor constraintEqualToConstant:57.5], // Title [self.titleLabel.topAnchor constraintEqualToAnchor:self.rootView.topAnchor constant:17], [self.titleLabel.leadingAnchor constraintEqualToAnchor:self.classicImageView.trailingAnchor constant:12], titleTrailingConstraint, // Description // - Top [self.descriptionLabel.topAnchor constraintEqualToAnchor:self.titleLabel.bottomAnchor constant:6], // - Horizontal [self.descriptionLabel.leadingAnchor constraintEqualToAnchor:self.titleLabel.leadingAnchor], [self.descriptionLabel.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor], // Link // - Horizontal [self.linkLabel.leadingAnchor constraintEqualToAnchor:self.titleLabel.leadingAnchor], [self.linkLabel.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor] ]; [NSLayoutConstraint activateConstraints:constraints]; self.descriptionConstraints = @[ [self.descriptionLabel.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor constant:-self.padding] ]; self.linkConstraints = @[ [self.linkLabel.topAnchor constraintEqualToAnchor:self.descriptionLabel.bottomAnchor constant:8], [self.linkLabel.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor constant:-self.padding] ]; } #pragma mark - ApplyCard - (void)applyCard:(ABKClassicContentCard *)card { if (![card isKindOfClass:[ABKClassicContentCard class]]) { return; } [super applyCard:card]; if (![Appboy sharedInstance].imageDelegate) { NSLog(@"[APPBOY][WARN] %@ %s", @"ImageDelegate on Appboy is nil. Image loading may be disabled.", __PRETTY_FUNCTION__); return; } [[Appboy sharedInstance].imageDelegate setImageForView:self.classicImageView showActivityIndicator:NO withURL:[NSURL URLWithString:card.image] imagePlaceHolder:[self getPlaceHolderImage] completed:nil]; } @end ================================================ FILE: AppboyUI/ABKContentCards/ViewControllers/Cells/ABKControlTableViewCell.h ================================================ #import #import "ABKBaseContentCardCell.h" NS_ASSUME_NONNULL_BEGIN @interface ABKControlTableViewCell : ABKBaseContentCardCell @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyUI/ABKContentCards/ViewControllers/Cells/ABKControlTableViewCell.m ================================================ #import "ABKControlTableViewCell.h" @implementation ABKControlTableViewCell @end ================================================ FILE: AppboyUI/ABKInAppMessage/ABKInAppMessageUIButton.h ================================================ #import #import "ABKInAppMessageButton.h" NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessageUIButton : UIButton /*! * The model object for the UIButton. */ @property ABKInAppMessageButton *inAppButtonModel; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyUI/ABKInAppMessage/ABKInAppMessageUIButton.m ================================================ #import "ABKInAppMessageUIButton.h" #import "ABKUIUtils.h" #define DefaultTitleSize UIFontTextStyleSubheadline static CGFloat const ButtonCornerRadius = 5.0f; static CGFloat const ButtonTitleSidePadding = 12.0; @interface ABKInAppMessageUIButton () @property (copy) UIColor *originalBackgroundColor; @end @implementation ABKInAppMessageUIButton - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setUp]; } return self; } - (instancetype)init { if (self = [super init]) { [self setUp]; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { if (self = [super initWithCoder:aDecoder]) { [self setUp]; } return self; } - (void)setUp { self.titleLabel.font = [ABKUIUtils preferredFontForTextStyle:DefaultTitleSize weight:UIFontWeightBold]; [ABKUIUtils enableAdjustsFontForContentSizeCategory:self.titleLabel]; self.titleLabel.textAlignment = NSTextAlignmentCenter; self.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail; self.originalBackgroundColor = self.backgroundColor; } - (void)layoutSubviews { [super layoutSubviews]; if ([ABKUIUtils objectIsValidAndNotEmpty:self.inAppButtonModel.buttonTextFont]) { self.titleLabel.font = self.inAppButtonModel.buttonTextFont; } if ([ABKUIUtils objectIsValidAndNotEmpty:self.inAppButtonModel.buttonTextColor]) { [self setTitleColor:self.inAppButtonModel.buttonTextColor forState:UIControlStateNormal]; } if ([ABKUIUtils objectIsValidAndNotEmpty:self.inAppButtonModel.buttonText]) { [self setTitle:self.inAppButtonModel.buttonText forState:UIControlStateNormal]; } if ([ABKUIUtils objectIsValidAndNotEmpty:self.inAppButtonModel.buttonBackgroundColor]) { self.backgroundColor = self.inAppButtonModel.buttonBackgroundColor; } if ([ABKUIUtils objectIsValidAndNotEmpty:self.inAppButtonModel.buttonBorderColor]) { self.layer.borderColor = [self.inAppButtonModel.buttonBorderColor CGColor]; } else if ([ABKUIUtils objectIsValidAndNotEmpty:self.inAppButtonModel.buttonBackgroundColor]) { self.layer.borderColor = [self.inAppButtonModel.buttonBackgroundColor CGColor]; } else { self.layer.borderColor = [[UIColor colorWithRed:(27.0/255.0) green:(120.0/255.0) blue:(207.0)/(255.0) alpha:1.0] CGColor]; } self.layer.cornerRadius = ButtonCornerRadius; self.titleLabel.frame = CGRectMake(ButtonTitleSidePadding, 0, self.bounds.size.width - ButtonTitleSidePadding * 2, self.bounds.size.height); } - (void)setHighlighted:(BOOL)highlighted { [super setHighlighted:highlighted]; if (highlighted) { [self setBackgroundColor:[UIColor colorWithRed:0 green:0 blue:0 alpha:.08]]; } else { self.backgroundColor = self.originalBackgroundColor; [self setNeedsLayout]; } } @end ================================================ FILE: AppboyUI/ABKInAppMessage/ABKInAppMessageUIController.h ================================================ #import #import "ABKInAppMessageUIControlling.h" #import "ABKInAppMessageUIDelegate.h" #import "ABKInAppMessageWindowController.h" @interface ABKInAppMessageUIController : NSObject /*! * supportedOrientationMask allows you to change which orientation mask the in-app message supports. * In-app messages will normally support the orientations specified in the app settings, but the method * supportedInterfaceOrientations may optionally override that. The value of supportedOrientationMask will be returned * in supportedInterfaceOrientations in the in-app message view controller. * * The default value of supportedOrientationMask is UIInterfaceOrientationMaskAll. */ @property UIInterfaceOrientationMask supportedOrientationMask; /*! * preferredOrientation allows you to select which orientation should be preferred if multiple orientations are supported by the view controller. * If set to a value other than UIInterfaceOrientationUnknown, the value of preferredOrientation will be returned by * preferredInterfaceOrientationForPresentation in the in-app message view controller. * Otherwise, the current status bar orientation will be returned. * * The default value of preferredOrientation is UIInterfaceOrientationUnknown, which means status bar orientation should be set * for in-app message orientation. */ @property UIInterfaceOrientation preferredOrientation; /*! * keyboardVisible will have the value YES when the keyboard is shown. */ @property BOOL keyboardVisible; /*! * The ABKInAppMessageWindowController that is being shown. */ @property (nullable) ABKInAppMessageWindowController *inAppMessageWindowController; /*! * The optional ABKInAppMessageUIDelegate that can be used to specify the UI behaviors of in-app messages. */ @property (weak, nullable) id uiDelegate; @end ================================================ FILE: AppboyUI/ABKInAppMessage/ABKInAppMessageUIController.m ================================================ #import "ABKInAppMessageUIController.h" #import "AppboyKit.h" #import "ABKInAppMessageWindowController.h" #import "ABKUIUtils.h" #import "ABKInAppMessageSlideupViewController.h" #import "ABKInAppMessageModalViewController.h" #import "ABKInAppMessageHTMLFullViewController.h" #import "ABKInAppMessageHTMLViewController.h" #import "ABKInAppMessageFullViewController.h" @implementation ABKInAppMessageUIController - (instancetype)init { if (self = [super init]) { _supportedOrientationMask = UIInterfaceOrientationMaskAll; _preferredOrientation = UIInterfaceOrientationUnknown; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveKeyboardWasShownNotification:) name:UIKeyboardWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveKeyboardDidHideNotification:) name:UIKeyboardDidHideNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inAppMessageWindowDismissed:) name:ABKNotificationInAppMessageWindowDismissed object:nil]; } return self; } #pragma mark - Show and Hide In-app Message - (void)showInAppMessage:(ABKInAppMessage *)inAppMessage { if ([UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPad) { // Check the device orientation before displaying the in-app message UIInterfaceOrientation statusBarOrientation = [ABKUIUtils getInterfaceOrientation]; NSString *errorMessage = @"The in-app message %@ with %@ orientation shouldn't be displayed in %@, disregarding this in-app message."; if (inAppMessage.orientation == ABKInAppMessageOrientationPortrait && !UIInterfaceOrientationIsPortrait(statusBarOrientation)) { NSLog(errorMessage, inAppMessage, @"portrait", @"landscape"); return; } if (inAppMessage.orientation == ABKInAppMessageOrientationLandscape && !UIInterfaceOrientationIsLandscape(statusBarOrientation)) { NSLog(errorMessage, inAppMessage, @"landscape", @"portrait"); return; } } if ([inAppMessage isKindOfClass:[ABKInAppMessageImmersive class]]) { ABKInAppMessageImmersive *immersiveInAppMessage = (ABKInAppMessageImmersive *)inAppMessage; if (immersiveInAppMessage.imageStyle == ABKInAppMessageGraphic && ![ABKUIUtils objectIsValidAndNotEmpty:immersiveInAppMessage.imageURI]) { NSLog(@"The in-app message has graphic image style but no image, discard this in-app message."); return; } if ([immersiveInAppMessage isKindOfClass:[ABKInAppMessageFull class]] && ![ABKUIUtils objectIsValidAndNotEmpty:immersiveInAppMessage.imageURI]) { NSLog(@"The in-app message is a full in-app message without an image, discard this in-app message."); return; } } if (inAppMessage.inAppMessageClickActionType == ABKInAppMessageNoneClickAction && [inAppMessage isKindOfClass:[ABKInAppMessageSlideup class]]) { ((ABKInAppMessageSlideup *)inAppMessage).hideChevron = YES; } ABKInAppMessageViewController *inAppMessageViewController = nil; if ([self.uiDelegate respondsToSelector:@selector(inAppMessageViewControllerWithInAppMessage:)]) { inAppMessageViewController = [self.uiDelegate inAppMessageViewControllerWithInAppMessage:inAppMessage]; } else { if ([inAppMessage isKindOfClass:[ABKInAppMessageSlideup class]]) { inAppMessageViewController = [[ABKInAppMessageSlideupViewController alloc] initWithInAppMessage:inAppMessage]; } else if ([inAppMessage isKindOfClass:[ABKInAppMessageModal class]]) { inAppMessageViewController = [[ABKInAppMessageModalViewController alloc] initWithInAppMessage:inAppMessage]; } else if ([inAppMessage isKindOfClass:[ABKInAppMessageFull class]]) { inAppMessageViewController = [[ABKInAppMessageFullViewController alloc] initWithInAppMessage:inAppMessage]; } else if ([inAppMessage isKindOfClass:[ABKInAppMessageHTMLFull class]]) { inAppMessageViewController = [[ABKInAppMessageHTMLFullViewController alloc] initWithInAppMessage:inAppMessage]; } else if ([inAppMessage isKindOfClass:[ABKInAppMessageHTML class]]) { inAppMessageViewController = [[ABKInAppMessageHTMLViewController alloc] initWithInAppMessage:inAppMessage]; } } if (inAppMessageViewController) { ABKInAppMessageWindowController *windowController = [[ABKInAppMessageWindowController alloc] initWithInAppMessage:inAppMessage inAppMessageViewController:inAppMessageViewController inAppMessageDelegate:self.uiDelegate]; windowController.supportedOrientationMask = self.supportedOrientationMask; windowController.preferredOrientation = self.preferredOrientation; self.inAppMessageWindowController = windowController; if (@available(iOS 13.0, *)) { inAppMessageViewController.overrideUserInterfaceStyle = inAppMessage.overrideUserInterfaceStyle; } [self.inAppMessageWindowController displayInAppMessageViewWithAnimation:inAppMessage.animateIn]; } } - (ABKInAppMessageDisplayChoice)getCurrentDisplayChoiceForInAppMessage:(ABKInAppMessage *)inAppMessage { ABKInAppMessageDisplayChoice inAppMessageDisplayChoice = self.keyboardVisible ? ABKDisplayInAppMessageLater : ABKDisplayInAppMessageNow; if (inAppMessageDisplayChoice == ABKDisplayInAppMessageLater) { NSLog(@"Initially setting in-app message display choice to ABKDisplayInAppMessageLater due to visible keyboard."); } if ([self.uiDelegate respondsToSelector:@selector(beforeInAppMessageDisplayed:withKeyboardIsUp:)]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" // ignore deprecation warning to support client integrations using the deprecated method inAppMessageDisplayChoice = [self.uiDelegate beforeInAppMessageDisplayed:inAppMessage withKeyboardIsUp:self.keyboardVisible]; #pragma clang diagnostic pop } else if ([[Appboy sharedInstance].inAppMessageController.delegate respondsToSelector:@selector(beforeInAppMessageDisplayed:)]) { inAppMessageDisplayChoice = [[Appboy sharedInstance].inAppMessageController.delegate beforeInAppMessageDisplayed:inAppMessage]; } return inAppMessageDisplayChoice; } - (ABKInAppMessageDisplayChoice)getCurrentDisplayChoiceForControlInAppMessage:(ABKInAppMessage *)controlInAppMessage { ABKInAppMessageDisplayChoice inAppMessageDisplayChoice = self.keyboardVisible ? ABKDisplayInAppMessageLater : ABKDisplayInAppMessageNow; if (inAppMessageDisplayChoice == ABKDisplayInAppMessageLater) { NSLog(@"Initially setting in-app message display choice to ABKDisplayInAppMessageLater due to visible keyboard."); } if ([[Appboy sharedInstance].inAppMessageController.delegate respondsToSelector:@selector(beforeControlMessageImpressionLogged:)]) { inAppMessageDisplayChoice = [Appboy.sharedInstance.inAppMessageController.delegate beforeControlMessageImpressionLogged:controlInAppMessage]; } return inAppMessageDisplayChoice; } - (BOOL)inAppMessageCurrentlyVisible { if (self.inAppMessageWindowController) { return YES; } return NO; } - (void)hideCurrentInAppMessage:(BOOL)animated { @try { if (self.inAppMessageWindowController) { [self.inAppMessageWindowController hideInAppMessageViewWithAnimation:animated]; } } @catch (NSException *exception) { NSLog(@"An error occured and this in-app message couldn't be hidden."); } } - (void)inAppMessageWindowDismissed:(NSNotification *)notification { // We listen to this notification so that we know when the screen is clear of in-app messages // and a new in-app message can be shown. self.inAppMessageWindowController = nil; } #pragma mark - Keyboard - (void)receiveKeyboardDidHideNotification:(NSNotification *)notification { self.keyboardVisible = NO; } - (void)receiveKeyboardWasShownNotification:(NSNotification *)notification { self.keyboardVisible = YES; [self.inAppMessageWindowController keyboardWasShown]; } #pragma mark - Set UIDelegate - (void)setInAppMessageUIDelegate:(id)uiDelegate { _uiDelegate = uiDelegate; } #pragma mark - Dealloc - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } @end ================================================ FILE: AppboyUI/ABKInAppMessage/ABKInAppMessageUIDelegate.h ================================================ #import #import #import "ABKInAppMessageViewController.h" #import "AppboyKit.h" NS_ASSUME_NONNULL_BEGIN /*! * The in-app message UI delegate allows you to control the display and behavior of the Braze in-app message. */ @protocol ABKInAppMessageUIDelegate @optional /*! * @param inAppMessage The in-app message object being offered to the delegate method. * @param keyboardIsUp This boolean indicates whether or not the keyboard is currently being displayed when this * delegate fires. * @return ABKInAppMessageDisplayChoice for details refer to the documentation regarding the ENUM ABKInAppMessageDisplayChoice * above. * * This delegate method defines whether the in-app message will be displayed now, displayed later, or discarded. * * The default behavior is that the in-app message will be displayed unless the keyboard is currently active on the screen. * However, if there are other situations where you would not want the in-app message to appear (such as during a full screen * game or on a loading screen), you can use this delegate to delay or discard pending in-app message messages. * * This method is deprecated. Please use the beforeInAppMessageDisplayed: method in ABKInAppMessageControllerDelegate * and use the methods receiveKeyboardDidHideNotification: and receiveKeyboardWasShownNotification: * in ABKInAppMessageUIController to customize based on keyboard behavior. */ - (ABKInAppMessageDisplayChoice)beforeInAppMessageDisplayed:(ABKInAppMessage *)inAppMessage withKeyboardIsUp:(BOOL)keyboardIsUp __deprecated; /*! * @param inAppMessage The in-app message object being offered to the delegate. * * This delegate method allows host applications to customize the look of an in-app message while * maintaining the same user experience and impression/click tracking as the default Braze in-app * message. It allows developers to pass incoming in-app messages to custom view controllers which * they have created. * * The custom view controller is responsible for handling any responsive UI layout use-cases. e.g. device orientations, * or varied message lengths. * * Even with a custom view, by inheriting from ABKInAppMessageViewController, the in-app message will automatically animate and * dismiss according to the parameters of the provided ABKInAppMessage object. See ABKInAppMessage.h for more information. * * By default, Braze will add following functions/changes to the custom view controller, and animate * the in-app message on and off the screen, based on the class of the given in-app message: * * ABKInAppMessageSlideup: * * stretch/shrink the in-app message view's width to fix the screen's width. If you wish to * have margins between the in-app message and the edge of the screen, those must be incorporated * into the custom view controller itself. * * add the impression and click tracking for the in-app message * * when user clicks on the in-app message, call the onInAppMessageClicked:, and handle the click * behavior correspond to the in-app message's inAppMessageClickActionType property. * * add a pan gesture to the in-app message so user can swipe it away. * * ABKInAppMessageModal: * * make the in-app message clickable when there is no button(s) on it. * * put the in-app message in the center of the screen, and add a full screen background layer. * * ABKInAppMessageFull: * * make the in-app message clickable when there is no button(s) on it. * * stretch/shrink the in-app message view to fix the whole screen. * * @returns An ABKInAppMessageViewController subclass for which the view is an ABKInAppMessageView * instance or subclass. Returning nil will prevent the in-app message from displaying. */ - (nullable ABKInAppMessageViewController *)inAppMessageViewControllerWithInAppMessage:(ABKInAppMessage *)inAppMessage; /*! * @param inAppMessage The in-app message object being offered to the delegate. * * This delegate method is fired when: * * the user manually dismisses the in-app message. * * the in-app message times out and expires. * * the close button on a modal in-app message or a full in-app message is clicked. * Use this method to perform any custom logic that should execute after the in-app message has been * dismissed. */ - (void)onInAppMessageDismissed:(ABKInAppMessage *)inAppMessage; /*! * @param inAppMessage The in-app message object being offered to the delegate. * @return Boolean Value which controls whether or not Braze will execute the click action. Returning YES will prevent * Braze from performing the click action. Returning NO will cause Braze to execute the action defined in the * in-app message's inAppMessageClickActionType property after this delegate method is called. * * This delegate method is fired when the user clicks on a slideup in-app message, or a modal/full * in-app message without button(s) on it. See ABKInAppMessage.h for more information. */ - (BOOL)onInAppMessageClicked:(ABKInAppMessage *)inAppMessage; /*! * @param inAppMessage The in-app message object being offered to the delegate. * @param button The clicked button being offered to the delegate. * @return Boolean Value which controls whether or not Braze will execute the click action. Returning YES will prevent * Braze from performing the click action. Returning NO will cause Braze to execute the action defined in the * button's inAppMessageClickActionType property after this delegate method is called. * * This delegate method is fired whenever the user clicks a button on the in-app message. See * ABKInAppMessageBlock.h for more information. */ - (BOOL)onInAppMessageButtonClicked:(ABKInAppMessageImmersive *)inAppMessage button:(ABKInAppMessageButton *)button; /*! * @param inAppMessage The in-app message object being offered to the delegate. * @param clickedURL The URL that is clicked by user. * @param buttonId The buttonId within the clicked link being offered to the delegate. * @return Boolean Value which controls whether or not Braze will execute the click action. Returning YES will prevent * Braze from performing the click action. Returning NO will cause Braze to follow the link. * * This delegate method is fired whenever the user clicks a link on the HTML in-app message. See * ABKInAppMessageHTMLBase.h for more information. */ - (BOOL)onInAppMessageHTMLButtonClicked:(ABKInAppMessageHTMLBase *)inAppMessage clickedURL:(nullable NSURL *)clickedURL buttonID:(NSString *)buttonId; - (WKWebViewConfiguration *)setCustomWKWebViewConfiguration; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyUI/ABKInAppMessage/ABKInAppMessageView.h ================================================ #import NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessageView : UIView @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyUI/ABKInAppMessage/ABKInAppMessageView.m ================================================ #import "ABKInAppMessageView.h" @implementation ABKInAppMessageView @end ================================================ FILE: AppboyUI/ABKInAppMessage/ABKInAppMessageWindow.h ================================================ #import /*! * ABKInAppMessageWindow handles a subset of all touches. * * By default, touches not handled by ABKInAppMessageWindow are automatically passed to the next * UIWindow in the view hierarchy by UIKit. */ @interface ABKInAppMessageWindow : UIWindow /*! * ABKInAppMessageWindow handles all touch events when enabled, no touch events are passed to a next * UIWindow. */ @property (nonatomic) BOOL handleAllTouchEvents; @end ================================================ FILE: AppboyUI/ABKInAppMessage/ABKInAppMessageWindow.m ================================================ #import "ABKInAppMessageWindow.h" #import "ABKInAppMessageView.h" #import "ABKInAppMessageWindowController.h" #import "ABKInAppMessageHTMLBase.h" #import "ABKUIUtils.h" @implementation ABKInAppMessageWindow // Touches handled by ABKInAppMessageWindow: // - all if `handleAllTouchEvents == YES` // - in `ABKInAppMessageView` or one of its subviews // - all if displaying an HTML in-app message - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { // Get the view in the hierarchy that contains the point UIView *hitTestResult = [super hitTest:point withEvent:event]; // Always returns the view for HTML in-app messages if ([self.rootViewController isKindOfClass:[ABKInAppMessageWindowController class]]) { ABKInAppMessageWindowController *controller = (ABKInAppMessageWindowController *)self.rootViewController; if ([controller.inAppMessage isKindOfClass:[ABKInAppMessageHTMLBase class]]) { return hitTestResult; } } // Handles the touch event if (self.handleAllTouchEvents || [ABKUIUtils responderChainOf:hitTestResult hasKindOfClass:[ABKInAppMessageView class]]) { return hitTestResult; } return nil; } @end ================================================ FILE: AppboyUI/ABKInAppMessage/AppboyInAppMessage.h ================================================ #import "ABKInAppMessageUIButton.h" #import "ABKInAppMessageUIController.h" #import "ABKInAppMessageUIDelegate.h" #import "ABKInAppMessageView.h" #import "ABKInAppMessageWindow.h" #import "ABKInAppMessageFullViewController.h" #import "ABKInAppMessageHTMLFullViewController.h" #import "ABKInAppMessageHTMLViewController.h" #import "ABKInAppMessageHTMLBaseViewController.h" #import "ABKInAppMessageImmersiveViewController.h" #import "ABKInAppMessageModalViewController.h" #import "ABKInAppMessageSlideupViewController.h" #import "ABKInAppMessageViewController.h" #import "ABKInAppMessageWindowController.h" #import "ABKUIURLUtils.h" #import "ABKUIUtils.h" ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/ABKInAppMessageFullViewController.xib ================================================ ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/ABKInAppMessageModalViewController.xib ================================================ ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/ABKInAppMessageSlideupViewController.xib ================================================ ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/Base.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "Close"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/ar.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "لإغلاق"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/cs.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "zavřít"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/da.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "at lukke"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/de.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "schließen"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/en.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "Close"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/es-419.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "cerrar"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/es-MX.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "cerrar"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/es.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "cerrar"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/et.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "sulgema"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/fi.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "sulkea"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/fil.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "Isara"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/fr.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "fermer"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/he.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "לִסְגוֹר"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/hi.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "बंद करना"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/id.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "untuk menutup"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/it.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "chiudere"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/ja.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "閉じる"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/km.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "បិទ"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/ko.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "닫다"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/lo.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "ປິດ"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/ms.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "untuk menutup"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/my.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "ပိတ်ရန်"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/nb.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "å lukke"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/nl.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "sluiten"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/pl.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "zamknąć"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/pt-PT.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "fechar"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/pt.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "fechar"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/ru.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "закрывать"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/sv.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "stänga"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/th.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "ปิด"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/uk.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "закрити"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/vi.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "đóng"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/zh-HK.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "關閉"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/zh-Hans.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "关闭"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/zh-Hant.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "關閉"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/zh-TW.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "關閉"; ================================================ FILE: AppboyUI/ABKInAppMessage/Resources/zh.lproj/AppboyInAppMessageLocalizable.strings ================================================ "Appboy.in-app-message.close-button.title" = "关闭"; ================================================ FILE: AppboyUI/ABKInAppMessage/ViewControllers/ABKInAppMessageFullViewController.h ================================================ #import "ABKInAppMessageImmersiveViewController.h" NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessageFullViewController : ABKInAppMessageImmersiveViewController @property (weak, nonatomic) IBOutlet NSLayoutConstraint *closeXButtonTopConstraint; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyUI/ABKInAppMessage/ViewControllers/ABKInAppMessageFullViewController.m ================================================ #import "ABKInAppMessageFullViewController.h" #import "ABKInAppMessageViewController.h" #import "ABKInAppMessageImmersive.h" #import "ABKUIUtils.h" static const CGFloat FullViewInIPadCornerRadius = 8.0f; static const CGFloat MaxLongEdge = 720.0f; static const CGFloat MaxShortEdge = 450.0f; static const CGFloat CloseXPadding = 15.0f; @implementation ABKInAppMessageFullViewController - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; CGFloat maxWidth = MaxShortEdge; CGFloat maxHeight = MaxLongEdge; if (self.inAppMessage.orientation == ABKInAppMessageOrientationLandscape) { maxWidth = MaxLongEdge; maxHeight = MaxShortEdge; } if (self.isiPad) { NSArray *widthConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:[view(<=max)]" options:0 metrics:@{@"max" : @(maxWidth)} views:@{@"view" : self.view}]; NSArray *heightConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[view(<=max)]" options:0 metrics:@{@"max" : @(maxHeight)} views:@{@"view" : self.view}]; [self.view addConstraints:widthConstraints]; [self.view addConstraints:heightConstraints]; self.view.layer.cornerRadius = FullViewInIPadCornerRadius; self.view.layer.masksToBounds = YES; [self.view.superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(>=0)-[view]-(>=0)-|" options:0 metrics:nil views:@{@"view" : self.view}]]; } else { NSLayoutConstraint *leadConstraint = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view.superview attribute:NSLayoutAttributeLeading multiplier:1 constant:0.0]; NSLayoutConstraint *trailConstraint = [NSLayoutConstraint constraintWithItem:self.view.superview attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTrailing multiplier:1 constant:0.0]; [self.view.superview addConstraints:@[leadConstraint, trailConstraint]]; } NSString *heightVisualFormat = self.isiPad? @"V:|-(>=0)-[view]-(>=0)-|" : @"V:|[view]|"; NSArray *heightConstraints = [NSLayoutConstraint constraintsWithVisualFormat:heightVisualFormat options:0 metrics:nil views:@{@"view" : self.view}]; [self.view.superview addConstraints:heightConstraints]; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; // Close X should be equidistant from top and right in notched phones despite presence of (hidden) status bar if (![ABKUIUtils isNotchedPhone]) { if (!self.isiPad) { CGSize statusBarSize = [ABKUIUtils getStatusBarSize]; self.closeXButtonTopConstraint.constant = CloseXPadding - statusBarSize.height; } } else { // Move close x button slightly higher for notched phones in portrait BOOL isPortrait = UIInterfaceOrientationIsPortrait([ABKUIUtils getInterfaceOrientation]); self.closeXButtonTopConstraint.constant = isPortrait ? 0.0f : CloseXPadding; } } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self.textsView flashScrollIndicators]; } - (void)loadView { NSBundle *bundle = [ABKUIUtils bundle:[ABKInAppMessageFullViewController class] channel:ABKInAppMessageChannel]; [bundle loadNibNamed:@"ABKInAppMessageFullViewController" owner:self options:nil]; self.inAppMessageHeaderLabel.font = HeaderLabelDefaultFont; self.inAppMessageMessageLabel.font = MessageLabelDefaultFont; if (self.inAppMessage.message) { NSMutableAttributedString *attributedStringMessage = [[NSMutableAttributedString alloc] initWithString:self.inAppMessage.message]; NSMutableParagraphStyle *messageStyle = [[NSMutableParagraphStyle alloc] init]; [messageStyle setLineSpacing:2]; [attributedStringMessage addAttribute:NSParagraphStyleAttributeName value:messageStyle range:NSMakeRange(0, self.inAppMessage.message.length)]; self.inAppMessageMessageLabel.attributedText = attributedStringMessage; } if ([self.inAppMessage isKindOfClass:[ABKInAppMessageImmersive class]]) { if (((ABKInAppMessageImmersive *)self.inAppMessage).header) { NSMutableAttributedString *attributedStringHeader = [[NSMutableAttributedString alloc] initWithString:((ABKInAppMessageImmersive *)self.inAppMessage).header]; NSMutableParagraphStyle *headerStyle = [[NSMutableParagraphStyle alloc] init]; [headerStyle setLineSpacing:2]; [attributedStringHeader addAttribute:NSParagraphStyleAttributeName value:headerStyle range:NSMakeRange(0, ((ABKInAppMessageImmersive *)self.inAppMessage).header.length)]; self.inAppMessageHeaderLabel.attributedText = attributedStringHeader; } } } #pragma mark - Superclass methods - (BOOL)prefersStatusBarHidden { return YES; } - (UIView *)bottomViewWithNoButton { return self.textsView; } - (void)setupLayoutForGraphic { [super applyImageToImageView:self.graphicImageView]; [self.iconImageView removeFromSuperview]; [self.textsView removeFromSuperview]; self.iconImageView = nil; self.textsView = nil; } - (void)setupLayoutForTopImage { [self.graphicImageView removeFromSuperview]; self.graphicImageView = nil; self.inAppMessageMessageLabel.translatesAutoresizingMaskIntoConstraints = NO; self.textsView.translatesAutoresizingMaskIntoConstraints = NO; // When there is no header, we set following two things to 0: // (1) the header label's height // (2) the constraint's height between header label and the message label // so that the space is collapsed. if (![ABKUIUtils objectIsValidAndNotEmpty:((ABKInAppMessageImmersive *)self.inAppMessage).header]) { for (NSLayoutConstraint *constraint in self.inAppMessageHeaderLabel.constraints) { if (constraint.firstAttribute == NSLayoutAttributeHeight) { constraint.constant = 0.0f; break; } } self.headerBodySpaceConstraint.constant = 0.0f; } [super applyImageToImageView:self.iconImageView]; } @end ================================================ FILE: AppboyUI/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLBaseViewController.h ================================================ #import #import #import "ABKInAppMessageViewController.h" NS_ASSUME_NONNULL_BEGIN static NSString *const ABKInAppMessageHTMLFileName = @"message.html"; @interface ABKInAppMessageHTMLBaseViewController : ABKInAppMessageViewController /*! * The WKWebView used to parse and display the HTML. */ @property (nonatomic) WKWebView *webView; /*! * The constraints for top and bottom between view and the super view. */ @property (weak, nonatomic) IBOutlet NSLayoutConstraint *topConstraint; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomConstraint; /*! * The flag specifying if body clicks should be registered automatically. Defaults to NO. */ @property (assign, nonatomic, readonly) BOOL automaticBodyClicksEnabled; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyUI/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLBaseViewController.m ================================================ #import "ABKInAppMessageHTMLBaseViewController.h" #import "ABKInAppMessageView.h" #import "ABKUIUtils.h" #import "ABKInAppMessageWindowController.h" #import "ABKInAppMessageWebViewBridge.h" #import "ABKUIURLUtils.h" static NSString *const ABKBlankURLString = @"about:blank"; static NSString *const ABKHTMLInAppButtonIdKey = @"abButtonId"; static NSString *const ABKHTMLInAppAppboyKey = @"appboy"; static NSString *const ABKHTMLInAppCloseKey = @"close"; static NSString *const ABKHTMLInAppFeedKey = @"feed"; static NSString *const ABKHTMLInAppCustomEventKey = @"customEvent"; static NSString *const ABKHTMLInAppCustomEventQueryParamNameKey = @"name"; static NSString *const ABKHTMLInAppExternalOpenKey = @"abExternalOpen"; static NSString *const ABKHTMLInAppDeepLinkKey = @"abDeepLink"; static NSString *const ABKHTMLInAppJavaScriptExtension = @"js"; @interface ABKInAppMessageHTMLBaseViewController () @property (nonatomic) ABKInAppMessageWebViewBridge *webViewBridge; @end @implementation ABKInAppMessageHTMLBaseViewController #pragma mark - Properties - (BOOL)automaticBodyClicksEnabled { return NO; } #pragma mark - View Lifecycle - (void)loadView { // View needs to be an ABKInAppMessageView to ensure touches register as per custom logic // in ABKInAppMessageWindow. The frame is set in `beforeMoveInAppMessageViewOnScreen`. self.view = [[ABKInAppMessageView alloc] initWithFrame:CGRectZero]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.view.translatesAutoresizingMaskIntoConstraints = NO; NSLayoutConstraint *leadConstraint = [self.view.leadingAnchor constraintEqualToAnchor:self.view.superview.leadingAnchor]; NSLayoutConstraint *trailConstraint = [self.view.trailingAnchor constraintEqualToAnchor:self.view.superview.trailingAnchor]; // Top and bottom constants will be populated with the actual frame sizes after // the HTML content is fully loaded in `beforeMoveInAppMessageViewOnScreen` #if TARGET_OS_MACCATALYST // Within safe zone self.topConstraint = [self.view.topAnchor constraintEqualToAnchor:self.view.superview.layoutMarginsGuide.topAnchor]; self.bottomConstraint = [self.view.bottomAnchor constraintEqualToAnchor:self.view.superview.layoutMarginsGuide.bottomAnchor]; #else // Extends to the edges of the screen self.topConstraint = [self.view.topAnchor constraintEqualToAnchor:self.view.superview.topAnchor]; self.bottomConstraint = [self.view.bottomAnchor constraintEqualToAnchor:self.view.superview.bottomAnchor]; #endif [self.view.superview addConstraints:@[leadConstraint, trailConstraint, self.topConstraint, self.bottomConstraint]]; } - (void)viewDidLoad { [super viewDidLoad]; self.edgesForExtendedLayout = UIRectEdgeNone; WKWebViewConfiguration *webViewConfiguration = [[WKWebViewConfiguration alloc] init]; webViewConfiguration.allowsInlineMediaPlayback = YES; webViewConfiguration.suppressesIncrementalRendering = YES; if (@available(iOS 10.0, *)) { webViewConfiguration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll; } else { webViewConfiguration.requiresUserActionForMediaPlayback = YES; } ABKInAppMessageWindowController *parentViewController = (ABKInAppMessageWindowController *)self.parentViewController; if ([parentViewController.inAppMessageUIDelegate respondsToSelector:@selector(setCustomWKWebViewConfiguration)]) { webViewConfiguration = [parentViewController.inAppMessageUIDelegate setCustomWKWebViewConfiguration]; } WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:webViewConfiguration]; self.webView = webView; self.webViewBridge = [[ABKInAppMessageWebViewBridge alloc] initWithWebView:webView inAppMessage:(ABKInAppMessageHTML *)self.inAppMessage appboyInstance:[Appboy sharedInstance]]; self.webViewBridge.delegate = self; self.webView.allowsLinkPreview = NO; self.webView.navigationDelegate = self; self.webView.UIDelegate = self; self.webView.scrollView.bounces = NO; // Handle resizing during orientation changes self.webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; if (@available(iOS 11.0, *)) { // Cover status bar when showing HTML IAMs [self.webView.scrollView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever]; } if (((ABKInAppMessageHTMLBase *)self.inAppMessage).assetsLocalDirectoryPath != nil) { NSString *localPath = [((ABKInAppMessageHTMLBase *)self.inAppMessage).assetsLocalDirectoryPath absoluteString]; // Here we must use fileURLWithPath: to add the "file://" scheme, otherwise the webView won't recognize the // base URL and won't load the zip file resources. NSURL *html = [NSURL fileURLWithPath:[localPath stringByAppendingPathComponent:ABKInAppMessageHTMLFileName]]; NSString *fullPath = [localPath stringByAppendingPathComponent:ABKInAppMessageHTMLFileName]; if (![[NSFileManager defaultManager] fileExistsAtPath:fullPath]) { NSLog(@"Can't find HTML at path %@, with file name %@. Aborting display.", [NSURL fileURLWithPath:localPath], ABKInAppMessageHTMLFileName); [self hideInAppMessage:NO]; } [self.webView loadFileURL:html allowingReadAccessToURL:[NSURL fileURLWithPath:localPath]]; } else { [self.webView loadHTMLString:self.inAppMessage.message baseURL:nil]; } [self.view addSubview:self.webView]; // Sets an observer for UIKeyboardWillHideNotification. This is a workaround for the // keyboard dismissal bug in iOS 12+ WKWebView filed here // https://bugs.webkit.org/show_bug.cgi?id=192564. The workaround is also from the post. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide) name:UIKeyboardWillHideNotification object:nil]; } #pragma mark - Superclass methods - (BOOL)prefersStatusBarHidden { return YES; } #pragma mark - NSNotificationCenter selectors - (void)keyboardWillHide { [self.webView setNeedsLayout]; } #pragma mark - WKDelegate methods - (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures { if (navigationAction.targetFrame == nil) { [webView loadRequest:navigationAction.request]; } return nil; } - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { NSURL *url = navigationAction.request.URL; // Handle normal html resource loading BOOL isSystemOpen = [ABKUIURLUtils URLHasSystemScheme:url]; BOOL isIframeLoad = navigationAction.targetFrame != nil && ![navigationAction.sourceFrame isEqual:navigationAction.targetFrame]; NSString *assetPath = ((ABKInAppMessageHTMLBase *)self.inAppMessage).assetsLocalDirectoryPath.absoluteString; BOOL isHandledByWebView = !isSystemOpen && ( !url || isIframeLoad || [ABKUIUtils string:url.absoluteString isEqualToString:ABKBlankURLString] || [ABKUIUtils string:url.path isEqualToString:assetPath] || [ABKUIUtils string:url.lastPathComponent isEqualToString:ABKInAppMessageHTMLFileName] ); if (isHandledByWebView) { decisionHandler(WKNavigationActionPolicyAllow); return; } // Handle Braze specific actions NSDictionary *queryParams = [self queryParameterDictionaryFromURL:url]; NSString *buttonId = [self parseButtonIdFromQueryParams:queryParams]; ABKInAppMessageWindowController *parentViewController = (ABKInAppMessageWindowController *)self.parentViewController; [self setClickActionBasedOnURL:url]; parentViewController.clickedHTMLButtonId = buttonId; // - Delegate handling if ([self delegateHandlesHTMLButtonClick:parentViewController.inAppMessageUIDelegate URL:url buttonId:buttonId]) { decisionHandler(WKNavigationActionPolicyCancel); return; } // - Custom event handling if ([self isCustomEventURL:url]) { [self handleCustomEventWithQueryParams:queryParams]; decisionHandler(WKNavigationActionPolicyCancel); return; } // - Body click handling if (![ABKUIUtils objectIsValidAndNotEmpty:buttonId]) { if (self.automaticBodyClicksEnabled) { parentViewController.inAppMessageIsTapped = YES; NSLog(@"In-app message body click registered. Automatic body clicks are enabled."); } else { NSLog(@"In-app message body click not registered. Automatic body clicks are disabled."); } } [parentViewController inAppMessageClickedWithActionType:self.inAppMessage.inAppMessageClickActionType URL:url openURLInWebView:[self getOpenURLInWebView:queryParams]]; decisionHandler(WKNavigationActionPolicyCancel); } - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { self.webView.backgroundColor = [UIColor clearColor]; self.webView.opaque = NO; if (self.inAppMessage.animateIn) { [UIView animateWithDuration:InAppMessageAnimationDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ self.topConstraint.constant = 0; self.bottomConstraint.constant = 0; [self.view.superview layoutIfNeeded]; } completion:^(BOOL finished){ }]; } else { self.topConstraint.constant = 0; self.bottomConstraint.constant = 0; [self.view.superview layoutIfNeeded]; } // Disable touch callout from displaying link information [self.webView evaluateJavaScript:@"document.documentElement.style.webkitTouchCallout='none';" completionHandler:nil]; } - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(nonnull NSString *)message initiatedByFrame:(nonnull WKFrameInfo *)frame completionHandler:(nonnull void (^)(void))completionHandler { [self presentAlertWithMessage:message andConfiguration:^(UIAlertController *alert) { // Action labels matches Safari implementation // Close [alert addAction:[UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { completionHandler(); }]]; }]; } - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler { [self presentAlertWithMessage:message andConfiguration:^(UIAlertController *alert) { // Action labels matches Safari implementation // Cancel [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { completionHandler(NO); }]]; // OK [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { completionHandler(YES); }]]; }]; } - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler { [self presentAlertWithMessage:prompt andConfiguration:^(UIAlertController *alert) { // Action labels matches Safari implementation // Text field [alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) { textField.text = defaultText; }]; // Cancel [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { completionHandler(nil); }]]; // OK [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { completionHandler(alert.textFields[0].text); }]]; }]; } - (BOOL)isCustomEventURL:(NSURL *)url { return ([ABKUIUtils string:url.scheme.lowercaseString isEqualToString:ABKHTMLInAppAppboyKey] && [ABKUIUtils string:url.host isEqualToString:ABKHTMLInAppCustomEventKey]); } - (BOOL)getOpenURLInWebView:(NSDictionary *)queryParams { if ([queryParams[ABKHTMLInAppDeepLinkKey] boolValue] | [queryParams[ABKHTMLInAppExternalOpenKey] boolValue]) { return NO; } return self.inAppMessage.openUrlInWebView; } #pragma mark - Delegate - (BOOL)delegateHandlesHTMLButtonClick:(id)delegate URL:(NSURL *)url buttonId:(NSString *)buttonId { if ([delegate respondsToSelector:@selector(onInAppMessageHTMLButtonClicked:clickedURL:buttonID:)]) { if ([delegate onInAppMessageHTMLButtonClicked:(ABKInAppMessageHTMLBase *)self.inAppMessage clickedURL:url buttonID:buttonId]) { NSLog(@"No in-app message click action will be performed by Braze as in-app message delegate %@ returned YES in onInAppMessageHTMLButtonClicked:", delegate); return YES; } } return NO; } #pragma mark - Custom Event Handling - (void)handleCustomEventWithQueryParams:(NSDictionary *)queryParams { NSString *customEventName = [self parseCustomEventNameFromQueryParams:queryParams]; NSMutableDictionary *eventProperties = [self parseCustomEventPropertiesFromQueryParams:queryParams]; [[Appboy sharedInstance] logCustomEvent:customEventName withProperties:eventProperties]; } - (NSString *)parseCustomEventNameFromQueryParams:(NSDictionary *)queryParams { return queryParams[ABKHTMLInAppCustomEventQueryParamNameKey]; } - (NSMutableDictionary *)parseCustomEventPropertiesFromQueryParams:(NSDictionary *)queryParams { NSMutableDictionary *eventProperties = [queryParams mutableCopy]; [eventProperties removeObjectForKey:ABKHTMLInAppCustomEventQueryParamNameKey]; return eventProperties; } #pragma mark - Button Click Handling - (NSString *)parseButtonIdFromQueryParams:(NSDictionary *)queryParams { return queryParams[ABKHTMLInAppButtonIdKey]; } // Set the inAppMessage's click action type based on given URL. It's going to be three types: // * URL is appboy://close: set click action to be ABKInAppMessageNoneClickAction // * URL is appboy://feed: set click action to be ABKInAppMessageDisplayNewsFeed // * URL is anything else: set click action to be ABKInAppMessageRedirectToURI and the uri is the URL. - (void)setClickActionBasedOnURL:(NSURL *)url { if ([ABKUIUtils string:url.scheme.lowercaseString isEqualToString:ABKHTMLInAppAppboyKey]) { if ([ABKUIUtils string:url.host.lowercaseString isEqualToString:ABKHTMLInAppCloseKey]) { [self.inAppMessage setInAppMessageClickAction:ABKInAppMessageNoneClickAction withURI:nil]; return; } else if ([ABKUIUtils string:url.host.lowercaseString isEqualToString:ABKHTMLInAppFeedKey]) { [self.inAppMessage setInAppMessageClickAction:ABKInAppMessageDisplayNewsFeed withURI:nil]; return; } } [self.inAppMessage setInAppMessageClickAction:ABKInAppMessageRedirectToURI withURI:url]; } #pragma mark - Utility Methods - (NSDictionary *)queryParameterDictionaryFromURL:(NSURL *)url { NSMutableDictionary *dict = [NSMutableDictionary dictionary]; NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; for (NSURLQueryItem *queryItem in components.queryItems) { dict[queryItem.name] = queryItem.value; } return [dict copy]; } - (void)presentAlertWithMessage:(NSString *)message andConfiguration:(void (^)(UIAlertController *alert))configure { UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil message:message preferredStyle:UIAlertControllerStyleAlert]; configure(alert); [self presentViewController:alert animated:YES completion:nil]; } #pragma mark - Animation - (void)beforeMoveInAppMessageViewOnScreen { self.topConstraint.constant = self.view.frame.size.height; self.bottomConstraint.constant = self.view.frame.size.height; } - (void)moveInAppMessageViewOnScreen { // Do nothing - moving the in-app message is handled in didFinishNavigation // though that logic should probably be gated by a call here. In a perfect world, // ABKInAppMessageWindowController would "request" VC's to show themselves, // and the VC's would report when they were shown so ABKInAppMessageWindowController // could log impressions. } - (void)beforeMoveInAppMessageViewOffScreen { self.topConstraint.constant = self.view.frame.size.height; self.bottomConstraint.constant = self.view.frame.size.height; } - (void)moveInAppMessageViewOffScreen { [self.view.superview layoutIfNeeded]; } #pragma mark - ABKInAppMessageWebViewBridgeDelegate - (void)webViewBridge:(ABKInAppMessageWebViewBridge *)webViewBridge receivedClickAction:(ABKInAppMessageClickActionType)clickAction { ABKInAppMessageWindowController *parentViewController = (ABKInAppMessageWindowController *)self.parentViewController; [self.inAppMessage setInAppMessageClickAction:clickAction withURI:nil]; [parentViewController inAppMessageClickedWithActionType:self.inAppMessage.inAppMessageClickActionType URL:nil openURLInWebView:false]; } - (void)closeMessageWithWebViewBridge:(ABKInAppMessageWebViewBridge *)webViewBridge { ABKInAppMessageWindowController *parentViewController = (ABKInAppMessageWindowController *)self.parentViewController; if ([parentViewController.inAppMessageUIDelegate respondsToSelector:@selector(onInAppMessageDismissed:)]) { [parentViewController.inAppMessageUIDelegate onInAppMessageDismissed:self.inAppMessage]; } [super hideInAppMessage:self.inAppMessage.animateOut]; } @end ================================================ FILE: AppboyUI/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLFullViewController.h ================================================ #import #import "ABKInAppMessageHTMLBaseViewController.h" NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessageHTMLFullViewController : ABKInAppMessageHTMLBaseViewController @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyUI/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLFullViewController.m ================================================ #import "ABKInAppMessageHTMLFullViewController.h" /*! * Custom implementation for the zip-based HTML IAM type */ @implementation ABKInAppMessageHTMLFullViewController - (BOOL)automaticBodyClicksEnabled { return YES; } @end ================================================ FILE: AppboyUI/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLViewController.h ================================================ #import #import "ABKInAppMessageHTMLBaseViewController.h" NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessageHTMLViewController : ABKInAppMessageHTMLBaseViewController @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyUI/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLViewController.m ================================================ #import "ABKInAppMessageHTMLViewController.h" /*! * Custom implementation for the file-based HTML IAM type */ @implementation ABKInAppMessageHTMLViewController @end ================================================ FILE: AppboyUI/ABKInAppMessage/ViewControllers/ABKInAppMessageImmersiveViewController.h ================================================ #import "ABKInAppMessageViewController.h" #import "ABKInAppMessageUIButton.h" // Customize this to set the font for the in-app message header. #define HeaderLabelDefaultFont [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleTitle3 weight:UIFontWeightBold] NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessageImmersiveViewController : ABKInAppMessageViewController /*! * The UILabel for the in-app message header. */ @property (weak, nonatomic) IBOutlet UILabel *inAppMessageHeaderLabel; /*! * The UIImageView for the in-app message image. */ @property (weak, nonatomic, nullable) IBOutlet UIImageView *graphicImageView; /*! * The NSLayoutConstraint that specifies the space between the header and rest of the in-app message. */ @property (nonatomic) IBOutlet NSLayoutConstraint *headerBodySpaceConstraint; /*! * The UIButton on the left of the in-app message. */ @property (retain, nonatomic, nullable) IBOutlet ABKInAppMessageUIButton *leftInAppMessageButton; /*! * The UIButton on the right of the in-app message. * When there is only one button in the in-app message, this right button is the one that is used. */ @property (retain, nonatomic, nullable) IBOutlet ABKInAppMessageUIButton *rightInAppMessageButton; /*! * The UIScrollView for the message of the in-app message. */ @property (nonatomic, nullable) IBOutlet UIScrollView *textsView; /*! * @discussion This method is used for setting up the layout for ABKInAppMessageGraphic image style. * * For customization, please use a subclass or category to override this method. */ - (void)setupLayoutForGraphic; /*! * @discussion This method is used for setting up the layout for ABKInAppMessageTopImage image style. * * For customization, please use a subclass or category to override this method. */ - (void)setupLayoutForTopImage; /*! * @discussion This method is used for setting the color of the close button. * * For customization, please use a subclass or category to override this method. */ - (void)changeCloseButtonColor; /*! * @discussion The touch up inside action for the close button. The default behavior is to close the * in-app message. * * For customization, please use a subclass or category to override this method. */ - (IBAction)dismissInAppMessage:(id)sender; /*! * @discussion The touch up inside action for the in-app message buttons. * * For customization, please use a subclass or category to override this method. */ - (IBAction)buttonClicked:(ABKInAppMessageUIButton *)button; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyUI/ABKInAppMessage/ViewControllers/ABKInAppMessageImmersiveViewController.m ================================================ #import "ABKInAppMessageImmersiveViewController.h" #import "ABKInAppMessageWindowController.h" #import "ABKUIUtils.h" static NSInteger const CloseButtonTag = 50; @implementation ABKInAppMessageImmersiveViewController #pragma mark - Immersive In-App Message View UI Initialization - (void)viewDidLoad { [super viewDidLoad]; [ABKUIUtils enableAdjustsFontForContentSizeCategory:self.inAppMessageMessageLabel]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.view.translatesAutoresizingMaskIntoConstraints = NO; ABKInAppMessageImmersive *inAppMessage = [self getInAppMessage]; self.inAppMessageHeaderLabel.text = inAppMessage.header; self.inAppMessageHeaderLabel.textAlignment = inAppMessage.headerTextAlignment; self.graphicImageView.contentMode = self.inAppMessage.imageContentMode; if (inAppMessage.headerTextColor != nil) { [self.inAppMessageHeaderLabel setTextColor:inAppMessage.headerTextColor]; } [self changeCloseButtonColor]; [self setCloseButtonAccessibilityLabel]; if (inAppMessage.imageStyle == ABKInAppMessageGraphic) { [self setupLayoutForGraphic]; } else { [self setupLayoutForTopImage]; } [self setupButtons]; if (![inAppMessage isKindOfClass:[ABKInAppMessageFull class]]) { if (inAppMessage.frameColor != nil) { self.view.superview.backgroundColor = inAppMessage.frameColor; } else { self.view.superview.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:.3]; } } NSLayoutConstraint *centerYConstraint = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.view.superview attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]; centerYConstraint.priority = 999; [self.view.superview addConstraint:centerYConstraint]; [self.view.superview addConstraint:[NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view.superview attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]]; self.view.alpha = 0.0f; } - (nullable UIButton *)getCloseButton { UIView *buttonView = [self.view viewWithTag:CloseButtonTag]; if ([buttonView isKindOfClass:[UIButton class]]) { return (UIButton *) buttonView; } return nil; } - (void)changeCloseButtonColor { UIButton *closeButton = [self getCloseButton]; if (closeButton != nil) { UIColor *closeButtonColor = [self getInAppMessage].closeButtonColor ? [self getInAppMessage].closeButtonColor : [UIColor colorWithRed:(155.0/255.0) green:(155.0/255.0) blue:(155.0/255.0) alpha:1.0]; UIImageView *closeButtonImageView = closeButton.imageView; closeButtonImageView.image = [closeButtonImageView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; closeButtonImageView.tintColor = closeButtonColor; [closeButton setImage:closeButtonImageView.image forState:UIControlStateNormal]; // Copy of the imageView for the Selected state UIImageView *closeButtonSelectedImageView = [[UIImageView alloc] initWithImage:closeButton.imageView.image]; closeButtonSelectedImageView.tintColor = [closeButtonColor colorWithAlphaComponent:InAppMessageSelectedOpacity]; [closeButton setImage:closeButtonSelectedImageView.image forState:UIControlStateSelected]; } } - (void)setCloseButtonAccessibilityLabel { UIButton *closeButton = [self getCloseButton]; if (closeButton != nil) { closeButton.accessibilityLabel = [self localizedAppboyInAppMessageString:@"Appboy.in-app-message.close-button.title"]; } } - (NSString *)localizedAppboyInAppMessageString:(NSString *)key { return [ABKUIUtils getLocalizedString:key inAppboyBundle:[ABKUIUtils bundle:[ABKInAppMessageImmersiveViewController class] channel:ABKInAppMessageChannel] table:@"AppboyInAppMessageLocalizable"]; } - (void)setupLayoutForGraphic { NSLog(@"Please override method setupLayoutForGraphic: to create proper layout for graphic image style."); } - (void)setupLayoutForTopImage { NSLog(@"Please override method setupLayoutForTopImage: to create proper layout for top image style."); } - (void)setupButtons { NSArray *buttons = [self getInAppMessage].buttons; if (![ABKUIUtils objectIsValidAndNotEmpty:buttons]) { [self.leftInAppMessageButton removeFromSuperview]; [self.rightInAppMessageButton removeFromSuperview]; self.leftInAppMessageButton = nil; self.rightInAppMessageButton = nil; if (([[self getInAppMessage] isKindOfClass:[ABKInAppMessageModal class]] || [[self getInAppMessage] isKindOfClass:[ABKInAppMessageFull class]]) && [self getInAppMessage].imageStyle != ABKInAppMessageGraphic) { UIView *bottomView = [self bottomViewWithNoButton]; if ([ABKUIUtils objectIsValidAndNotEmpty:bottomView]) { NSArray *bottomConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[view]-30-|" options:0 metrics:nil views:@{@"view" : bottomView}]; [self.view addConstraints:bottomConstraints]; } } } else if (buttons.count == 1) { [self.leftInAppMessageButton removeFromSuperview]; self.leftInAppMessageButton = nil; self.rightInAppMessageButton.inAppButtonModel = buttons[0]; NSLayoutConstraint *constraintHorizontal = [NSLayoutConstraint constraintWithItem:self.rightInAppMessageButton attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1.0f constant:0.0f]; [self.view addConstraint:constraintHorizontal]; } else { self.leftInAppMessageButton.inAppButtonModel = buttons[0]; self.rightInAppMessageButton.inAppButtonModel = buttons[1]; } } - (void)setInAppMessage:(ABKInAppMessage *)inAppMessage { if ([inAppMessage isKindOfClass:[ABKInAppMessageImmersive class]]) { super.inAppMessage = inAppMessage; } else { NSLog(@"ABKInAppMessageImmersiveViewController only accepts in-app message with type ABKInAppMessageImmersive. Setting in-app message fails."); } } - (UIView *)bottomViewWithNoButton { return nil; } #pragma mark - Animation - (void)moveInAppMessageViewOnScreen { self.view.alpha = 1.0f; } - (void)moveInAppMessageViewOffScreen { self.view.alpha = 0.0f; } #pragma mark - Button Actions - (IBAction)dismissInAppMessage:(id)sender { ABKInAppMessageWindowController *parentViewController = (ABKInAppMessageWindowController *)self.parentViewController; if ([parentViewController.inAppMessageUIDelegate respondsToSelector:@selector(onInAppMessageDismissed:)]) { [parentViewController.inAppMessageUIDelegate onInAppMessageDismissed:self.inAppMessage]; } [super hideInAppMessage:self.inAppMessage.animateOut]; } - (IBAction)buttonClicked:(ABKInAppMessageUIButton *)button { ABKInAppMessageWindowController *parentViewController = (ABKInAppMessageWindowController *)self.parentViewController; parentViewController.clickedButtonId = button.inAppButtonModel.buttonID; // Calls the delegate method for button click if it has been implemented. if ([parentViewController.inAppMessageUIDelegate respondsToSelector:@selector(onInAppMessageButtonClicked:button:)]) { if ([parentViewController.inAppMessageUIDelegate onInAppMessageButtonClicked:(ABKInAppMessageImmersive *)self.inAppMessage button:button.inAppButtonModel]) { NSLog(@"No in-app message click action will be performed by Braze as inAppMessageUIDelegate %@ returned YES in onInAppMessageButtonClicked:", parentViewController.inAppMessageUIDelegate); return; } } [parentViewController inAppMessageClickedWithActionType:button.inAppButtonModel.buttonClickActionType URL:button.inAppButtonModel.buttonClickedURI openURLInWebView:button.inAppButtonModel.buttonOpenUrlInWebView]; } #pragma mark - Get In-App Message - (ABKInAppMessageImmersive *)getInAppMessage { return (ABKInAppMessageImmersive *)self.inAppMessage; } #pragma mark - Dealloc - (void)dealloc { if ([ABKUIUtils objectIsValidAndNotEmpty:[self getInAppMessage].buttons]) { [self.leftInAppMessageButton removeTarget:self action:nil forControlEvents:UIControlEventAllEvents]; [self.rightInAppMessageButton removeTarget:self action:nil forControlEvents:UIControlEventAllEvents]; } } @end ================================================ FILE: AppboyUI/ABKInAppMessage/ViewControllers/ABKInAppMessageModalViewController.h ================================================ #import "ABKInAppMessageImmersiveViewController.h" NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessageModalViewController : ABKInAppMessageImmersiveViewController /*! * This boolean determines if the modal in-app message will be dismissed when the user taps outside of the * in-app message. * * @discussion The default of this value is NO but can be overriden by setting the value of ABKEnableDismissModalOnOutsideTapKey in * appboyOptions or in the Braze dictionary in your Info.plist file. */ @property (nonatomic, assign) BOOL enableDismissOnOutsideTap; /*! * The NSLayoutConstraint that specifies the height of the part of the in-app message which houses * the image. */ @property (retain, nonatomic) IBOutlet NSLayoutConstraint *iconImageHeightConstraint; @property (retain, nonatomic) IBOutlet NSLayoutConstraint *textsViewWidthConstraint; @property (strong, nonatomic) IBOutlet UIView *iconImageContainerView; @property (strong, nonatomic) IBOutlet UIView *graphicImageContainerView; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyUI/ABKInAppMessage/ViewControllers/ABKInAppMessageModalViewController.m ================================================ #import "ABKInAppMessageModalViewController.h" #import "ABKUIUtils.h" #import "ABKInAppMessageViewController.h" #import "ABKInAppMessageImmersive.h" #import "Appboy.h" #import "ABKInAppMessageController.h" #import "ABKImageDelegate.h" static const CGFloat ModalViewCornerRadius = 8.0f; static const CGFloat MaxModalViewWidth = 450.0f; static const CGFloat MinModalViewWidth = 320.0f; static const CGFloat MaxModalViewHeight = 720.0f; @implementation ABKInAppMessageModalViewController - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.enableDismissOnOutsideTap = [Appboy sharedInstance].inAppMessageController.enableDismissModalOnOutsideTap; if (((ABKInAppMessageImmersive *)self.inAppMessage).imageStyle == ABKInAppMessageTopImage) { [self.view.superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(>=15)-[view]-(>=15)-|" options:0 metrics:nil views:@{@"view" : self.view}]]; [self.view.superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(>=15)-[view]-(>=15)-|" options:0 metrics:nil views:@{@"view" : self.view}]]; } else { @try { UIImage *inAppImage = [[Appboy sharedInstance].imageDelegate imageFromCacheForURL:self.inAppMessage.imageURI]; CGFloat imageAspectRatio = 1.0; if (inAppImage != nil) { imageAspectRatio = inAppImage.size.width / inAppImage.size.height; } NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self.graphicImageView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.graphicImageView attribute:NSLayoutAttributeHeight multiplier:imageAspectRatio constant:0]; [self.graphicImageView addConstraint:constraint]; NSArray *maxWidthConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"H:[view(<=max)]" options:0 metrics:@{@"max" : @(MaxModalViewWidth)} views:@{@"view" : self.graphicImageView}]; NSArray *maxHeightConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[view(<=max)]" options:0 metrics:@{@"max" : @(MaxModalViewHeight)} views:@{@"view" : self.graphicImageView}]; [self.graphicImageView addConstraints:maxWidthConstraint]; [self.graphicImageView addConstraints:maxHeightConstraint]; [self.view.superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(>=15)-[view]-(>=15)-|" options:0 metrics:nil views:@{@"view" : self.view}]]; [self.view.superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(>=15)-[view]-(>=15)-|" options:0 metrics:nil views:@{@"view" : self.view}]]; } @catch (NSException *exception) { NSLog(@"Braze cannot display this message because it has a height or width of 0. The graphic image has width %f and height %f and image URI %@.", self.graphicImageView.image.size.width, self.graphicImageView.image.size.height, self.inAppMessage.imageURI.absoluteString); [self hideInAppMessage:NO]; } } } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self.textsView flashScrollIndicators]; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; if (![self isKindOfClass:[ABKInAppMessageModalViewController class]]) { return; } [self drawShadows]; if (self.iconImageView) { // Clips the top corners if the image is wide enough in the VC CAShapeLayer * maskLayer = [CAShapeLayer layer]; UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.view.bounds byRoundingCorners:(UIRectCornerTopLeft | UIRectCornerTopRight) cornerRadii:CGSizeMake(ModalViewCornerRadius, ModalViewCornerRadius)]; maskLayer.path = maskPath.CGPath; self.iconImageContainerView.layer.mask = maskLayer; self.iconImageContainerView.clipsToBounds = YES; } if (self.textsView && !self.textsViewWidthConstraint) { [self addTextViewConstraints]; } [self.view layoutIfNeeded]; } - (void)loadView { NSBundle *bundle = [ABKUIUtils bundle:[ABKInAppMessageModalViewController class] channel:ABKInAppMessageChannel]; [bundle loadNibNamed:@"ABKInAppMessageModalViewController" owner:self options:nil]; self.view.layer.cornerRadius = ModalViewCornerRadius; self.inAppMessageHeaderLabel.font = HeaderLabelDefaultFont; self.inAppMessageMessageLabel.font = MessageLabelDefaultFont; if (self.inAppMessage.message) { NSMutableAttributedString *attributedStringMessage = [[NSMutableAttributedString alloc] initWithString:self.inAppMessage.message]; NSMutableParagraphStyle *messageStyle = [[NSMutableParagraphStyle alloc] init]; [messageStyle setLineSpacing:2]; [attributedStringMessage addAttribute:NSParagraphStyleAttributeName value:messageStyle range:NSMakeRange(0, self.inAppMessage.message.length)]; self.inAppMessageMessageLabel.attributedText = attributedStringMessage; } if ([self.inAppMessage isKindOfClass:[ABKInAppMessageImmersive class]]) { if (((ABKInAppMessageImmersive *)self.inAppMessage).header) { NSMutableAttributedString *attributedStringHeader = [[NSMutableAttributedString alloc] initWithString:((ABKInAppMessageImmersive *)self.inAppMessage).header]; NSMutableParagraphStyle *headerStyle = [[NSMutableParagraphStyle alloc] init]; [headerStyle setLineSpacing:2]; [attributedStringHeader addAttribute:NSParagraphStyleAttributeName value:headerStyle range:NSMakeRange(0, ((ABKInAppMessageImmersive *)self.inAppMessage).header.length)]; self.inAppMessageHeaderLabel.attributedText = attributedStringHeader; } } } #pragma mark - Private methods - (void)drawShadows { UIBezierPath *dropShadowPath = [UIBezierPath bezierPathWithRoundedRect:self.view.bounds cornerRadius:self.view.layer.cornerRadius]; self.view.layer.masksToBounds = NO; self.view.layer.shadowOffset = CGSizeMake(0.0f, 0.0f); self.view.layer.shadowRadius = InAppMessageShadowBlurRadius; self.view.layer.shadowColor = [[UIColor blackColor] colorWithAlphaComponent:InAppMessageShadowOpacity].CGColor; self.view.layer.shadowPath = dropShadowPath.CGPath; // Make opacity of shadow match opacity of the In-App Message background CGFloat alpha = 0; [self.view.backgroundColor getRed:nil green:nil blue:nil alpha:&alpha]; self.view.layer.shadowOpacity = alpha; } - (void)addTextViewConstraints { [self.view layoutIfNeeded]; NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:self.textsView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:self.textsView.contentSize.width]; self.textsViewWidthConstraint = widthConstraint; NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:self.textsView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:self.textsView.contentSize.height]; widthConstraint.priority = 999; heightConstraint.priority = 999; [self.textsView addConstraint:widthConstraint]; [self.textsView addConstraint:heightConstraint]; } #pragma mark - Superclass methods - (UIView *)bottomViewWithNoButton { return self.textsView; } - (void)setupLayoutForGraphic { [super applyImageToImageView:self.graphicImageView]; self.graphicImageContainerView.layer.cornerRadius = self.view.layer.cornerRadius; [self.iconImageView removeFromSuperview]; [self.iconImageContainerView removeFromSuperview]; [self.iconLabelView removeFromSuperview]; [self.textsView removeFromSuperview]; self.iconImageView = nil; self.iconLabelView = nil; self.inAppMessageHeaderLabel = nil; self.inAppMessageMessageLabel = nil; self.textsView = nil; } - (void)setupLayoutForTopImage { self.textsView.translatesAutoresizingMaskIntoConstraints = NO; [self.graphicImageView removeFromSuperview]; [self.graphicImageContainerView removeFromSuperview]; self.graphicImageView = nil; // Set up the icon image/label view if ([super applyImageToImageView:self.iconImageView]) { [self.iconLabelView removeFromSuperview]; self.iconLabelView = nil; @try { UIImage *inAppImage = [[Appboy sharedInstance].imageDelegate imageFromCacheForURL:self.inAppMessage.imageURI]; CGFloat imageAspectRatio = 1.0; if (inAppImage != nil) { imageAspectRatio = inAppImage.size.width / inAppImage.size.height; } NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self.iconImageView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.iconImageView attribute:NSLayoutAttributeHeight multiplier:imageAspectRatio constant:0]; [self.iconImageView addConstraint:constraint]; } @catch (NSException *exception) { NSLog(@"Braze cannot display this message because the image has a height or width of 0. The image has width %f and height %f and image URI %@.", self.iconImageView.image.size.width, self.iconImageView.image.size.height, self.inAppMessage.imageURI.absoluteString); [self hideInAppMessage:NO]; } } else { self.iconImageView.hidden = YES; self.iconImageHeightConstraint.constant = self.iconLabelView.frame.size.height + 20.0f; if (![super applyIconToLabelView:self.iconLabelView]) { // When there is no image or icon, remove the iconLabelView to free up the space of the image view [self.iconLabelView removeFromSuperview]; self.iconLabelView = nil; self.iconImageHeightConstraint.constant = 20.0f; } } if (![ABKUIUtils objectIsValidAndNotEmpty:((ABKInAppMessageImmersive *)self.inAppMessage).header]) { for (NSLayoutConstraint *constraint in self.inAppMessageHeaderLabel.constraints) { if (constraint.firstAttribute == NSLayoutAttributeHeight) { constraint.constant = 0.0f; break; } } self.headerBodySpaceConstraint.constant = 0.0f; } NSArray *maxWidthConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"H:[view(<=max)]" options:0 metrics:@{@"max" : @(MaxModalViewWidth)} views:@{@"view" : self.view}]; NSArray *minWidthConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"H:[view(>=min)]" options:0 metrics:@{@"min" : @(MinModalViewWidth)} views:@{@"view" : self.view}]; NSArray *maxHeightConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[view(<=max)]" options:0 metrics:@{@"max" : @(MaxModalViewHeight)} views:@{@"view" : self.view}]; [self.view addConstraints:maxWidthConstraint]; [self.view addConstraints:minWidthConstraint]; [self.view addConstraints:maxHeightConstraint]; } @end ================================================ FILE: AppboyUI/ABKInAppMessage/ViewControllers/ABKInAppMessageSlideupViewController.h ================================================ #import "ABKInAppMessageViewController.h" NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessageSlideupViewController : ABKInAppMessageViewController /*! * The UIImageView for the arrow of the in-app message. */ @property (weak, nonatomic, nullable) IBOutlet UIImageView *arrowImage; /*! * The offset which controls the slideup in-app message vertical position once visible. */ @property (assign, nonatomic) CGFloat offset; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyUI/ABKInAppMessage/ViewControllers/ABKInAppMessageSlideupViewController.m ================================================ #import "ABKInAppMessageSlideupViewController.h" #import "ABKInAppMessageSlideup.h" #import "ABKUIUtils.h" static CGFloat const AssetSideMargin = 20.0f; static CGFloat const DefaultViewRadius = 15.0f; static CGFloat const DefaultVerticalMarginHeight = 10.0f; @interface ABKInAppMessageSlideupViewController() @property (strong, nonatomic) NSLayoutConstraint *slideConstraint; @property (nonatomic, readonly) BOOL animatesFromTop; @property (nonatomic, readonly) CGFloat safeAreaOffset; @end @implementation ABKInAppMessageSlideupViewController - (void)loadView { NSBundle *bundle = [ABKUIUtils bundle:[ABKInAppMessageSlideupViewController class] channel:ABKInAppMessageChannel]; [bundle loadNibNamed:@"ABKInAppMessageSlideupViewController" owner:self options:nil]; self.inAppMessageMessageLabel.font = MessageLabelDefaultFont; if (self.inAppMessage.message) { NSMutableAttributedString *attributedStringMessage = [[NSMutableAttributedString alloc] initWithString:self.inAppMessage.message]; NSMutableParagraphStyle *messageStyle = [[NSMutableParagraphStyle alloc] init]; [messageStyle setLineSpacing:2]; [messageStyle setLineBreakMode:NSLineBreakByTruncatingTail]; [attributedStringMessage addAttribute:NSParagraphStyleAttributeName value:messageStyle range:NSMakeRange(0, self.inAppMessage.message.length)]; self.inAppMessageMessageLabel.attributedText = attributedStringMessage; } } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.view.translatesAutoresizingMaskIntoConstraints = NO; [self setupChevron]; [self setupImageOrLabelView]; self.view.layer.cornerRadius = DefaultViewRadius; self.view.layer.masksToBounds = NO; } - (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; // Setup the constraints once UIKit has set the layoutMargins / safeAreaInsets if (!self.slideConstraint) { [self setupConstraintsWithSuperView]; } } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; // Redraw the shadow when the layout is changed. UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRoundedRect:self.view.bounds cornerRadius:DefaultViewRadius]; self.view.layer.shadowColor = [[UIColor blackColor] colorWithAlphaComponent:InAppMessageShadowOpacity].CGColor; self.view.layer.shadowOffset = CGSizeMake(0.0f, 0.0f); self.view.layer.shadowRadius = InAppMessageShadowBlurRadius; self.view.layer.shadowPath = shadowPath.CGPath; // Make opacity of shadow match opacity of the In-App Message background CGFloat alpha = 0; [self.view.backgroundColor getRed:nil green:nil blue:nil alpha:&alpha]; self.view.layer.shadowOpacity = alpha; } #pragma mark - Public methods - (CGFloat)offset { return self.slideConstraint.constant - self.safeAreaOffset; } - (void)setOffset:(CGFloat)offset { self.slideConstraint.constant = offset + self.safeAreaOffset; } #pragma mark - Private methods - (void)setupChevron { if (((ABKInAppMessageSlideup *)self.inAppMessage).hideChevron) { [self.arrowImage removeFromSuperview]; self.arrowImage = nil; NSLayoutConstraint *inAppMessageLabelTrailingConstraint = [self.inAppMessageMessageLabel.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-AssetSideMargin]; [self.view addConstraint:inAppMessageLabelTrailingConstraint]; } else { if (((ABKInAppMessageSlideup *)self.inAppMessage).chevronColor != nil) { UIColor *arrowColor = ((ABKInAppMessageSlideup *)self.inAppMessage).chevronColor; self.arrowImage.image = [self.arrowImage.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; self.arrowImage.tintColor = arrowColor; } else { UIColor *defaultArrowColor = [UIColor colorWithRed:(155.0/255.0) green:(155.0/255.0) blue:(155.0/255.0) alpha:1.0]; self.arrowImage.image = [self.arrowImage.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; self.arrowImage.tintColor = defaultArrowColor; } } } - (void)setupImageOrLabelView { if (![super applyImageToImageView:self.iconImageView]) { [self.iconImageView removeFromSuperview]; self.iconImageView = nil; if (![super applyIconToLabelView:self.iconLabelView]) { [self.iconLabelView removeFromSuperview]; self.iconLabelView = nil; NSLayoutConstraint *inAppMessageLabelLeadingConstraint = [self.inAppMessageMessageLabel.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:AssetSideMargin]; [self.view addConstraint:inAppMessageLabelLeadingConstraint]; } } } - (void)setupConstraintsWithSuperView { NSLayoutConstraint *leadConstraint = [self.view.leadingAnchor constraintEqualToAnchor:self.view.superview.layoutMarginsGuide.leadingAnchor]; NSLayoutConstraint *trailConstraint = [self.view.trailingAnchor constraintEqualToAnchor:self.view.superview.layoutMarginsGuide.trailingAnchor]; NSLayoutConstraint *offscreenConstraint; if (self.animatesFromTop) { offscreenConstraint = [self.view.bottomAnchor constraintEqualToAnchor:self.view.superview.topAnchor]; self.slideConstraint = [self.view.topAnchor constraintEqualToAnchor:self.view.superview.layoutMarginsGuide.topAnchor constant:self.safeAreaOffset]; } else { offscreenConstraint = [self.view.topAnchor constraintEqualToAnchor:self.view.superview.bottomAnchor]; self.slideConstraint = [self.view.bottomAnchor constraintEqualToAnchor:self.view.superview.layoutMarginsGuide.bottomAnchor constant:self.safeAreaOffset]; } offscreenConstraint.priority = UILayoutPriorityDefaultLow; [NSLayoutConstraint activateConstraints:@[leadConstraint, trailConstraint, offscreenConstraint]]; } - (BOOL)animatesFromTop { return ((ABKInAppMessageSlideup *)self.inAppMessage).inAppMessageSlideupAnchor == ABKInAppMessageSlideupFromTop; } - (CGFloat)safeAreaOffset { BOOL hasSafeArea = self.animatesFromTop ? self.view.superview.layoutMargins.top != 0 : self.view.superview.layoutMargins.bottom != 0; if (hasSafeArea) { return 0; } return self.animatesFromTop ? DefaultVerticalMarginHeight : -DefaultVerticalMarginHeight; } #pragma mark - Superclass methods - (void)beforeMoveInAppMessageViewOnScreen { self.slideConstraint.active = YES; } - (void)moveInAppMessageViewOnScreen { [self.view.superview layoutIfNeeded]; } - (void)beforeMoveInAppMessageViewOffScreen { self.slideConstraint.active = NO; } - (void)moveInAppMessageViewOffScreen { [self.view.superview layoutIfNeeded]; } - (void)setInAppMessage:(ABKInAppMessage *)inAppMessage { if ([inAppMessage isKindOfClass:[ABKInAppMessageSlideup class]]) { super.inAppMessage = inAppMessage; } else { NSLog(@"ABKInAppMessageSlideupViewController only accepts in-app message with type ABKInAppMessageSlideup. Setting in-app message fails."); } } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { if (self.inAppMessage.inAppMessageClickActionType != ABKInAppMessageNoneClickAction) { self.view.alpha = InAppMessageSelectedOpacity; } } @end ================================================ FILE: AppboyUI/ABKInAppMessage/ViewControllers/ABKInAppMessageViewController.h ================================================ #import #import "ABKInAppMessage.h" // Customize this to set the font for the in-app message message. #define MessageLabelDefaultFont [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline] static const CGFloat InAppMessageShadowBlurRadius = 4.0f; static const CGFloat InAppMessageShadowOpacity = 0.3f; static const CGFloat InAppMessageSelectedOpacity = 0.8f; NS_ASSUME_NONNULL_BEGIN @interface ABKInAppMessageViewController : UIViewController /*! * The in-app message that is being displayed in the view controller. */ @property (strong) ABKInAppMessage *inAppMessage; /*! * The UIImageView for the in-app message image. */ @property (weak, nonatomic) IBOutlet UIImageView *iconImageView; /*! * The UILabel for the in-app message icon. */ @property (weak, nonatomic) IBOutlet UILabel *iconLabelView; /*! * The UILabel for the in-app message message. */ @property (weak, nonatomic) IBOutlet UILabel *inAppMessageMessageLabel; /*! * This is YES if the device being used is an iPad, and NO if the device is not an iPad. */ @property BOOL isiPad; /*! * @discussion This method is used for passing the in-app message property to any custom view * controller. */ - (instancetype)initWithInAppMessage:(ABKInAppMessage *)inAppMessage; /*! * @discussion This method is used to decide whether the in-app message will be animated off the screen. * If YES, the in-app message will animate off the screen. If NO, the in-app message will * disappear immediately without animation. * * For customization, please use a subclass or category to override this method. */ - (void)hideInAppMessage:(BOOL)animated; /* * @discussion This method is called right before an in-app message view is going to be animated and * removed from screen. You can use this method to change the in-app message view's * constraints and call the `layoutIfNeeded` method in the `moveInAppMessageViewOffScreen` * method to animate the constraint changes. * * For customization, please use a subclass or category to override this method. * You must implement this method in a custom view controller. * The default implementation of the method does nothing. */ - (void)beforeMoveInAppMessageViewOffScreen; /* * @discussion This method is called when an in-app message view is going to be removed from the * screen. You can use this method to control the in-app message view's * animation by setting the off-screen position and status of the in-app message view, for * example, by setting the alpha of the view to 0. * * For customization, please use a subclass or category to override this method. * You must implement this method in a custom view controller. * The default implementation of the method does nothing. */ - (void)moveInAppMessageViewOffScreen; /* * @discussion This method is called right before the in-app message view is going to be animated and * displayed on the screen. You can use this method to change the in-app message view's * constraints and call the `layoutIfNeeded` method in the `moveInAppMessageViewOnScreen` * method to animate the constraint changes. * * For customization, please use a subclass or category to override this method. * You must implement this method in a custom view controller. * The default implementation of the method does nothing. */ - (void)beforeMoveInAppMessageViewOnScreen; /* * @discussion This method is called when in-app message view is going to displayed on the screen. You * can use this method to control the in-app message view's animation by setting the on- * screen position and status of the in-app message view, for example by moving the in-app * message view to the center of the screen or setting the alpha of the view to 1. * * For customization, please use a subclass or category to override this method. * You must implement this method in a custom view controller. * The default implementation of the method does nothing. */ - (void)moveInAppMessageViewOnScreen; /* * @discussion This method sets the image of the in-app message. * * For customization, please use a subclass or category to override this method. */ - (BOOL)applyImageToImageView:(UIImageView *)iconImageView; /* * @discussion This method sets the icon of the in-app message. * * For customization, please use a subclass or category to override this method. */ - (BOOL)applyIconToLabelView:(UILabel *)iconLabelView; @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyUI/ABKInAppMessage/ViewControllers/ABKInAppMessageViewController.m ================================================ #import #import "ABKInAppMessageViewController.h" #import "ABKInAppMessageView.h" #import "ABKInAppMessageWindowController.h" #import "ABKUIUtils.h" #import "Appboy.h" static const float InAppMessageIconLabelCornerRadius_iPhone = 10.0f; static const float InAppMessageIconLabelCornerRadius_iPad = 15.0f; static NSString *const FontAwesomeName = @"FontAwesome"; @implementation ABKInAppMessageViewController - (instancetype)initWithInAppMessage:(ABKInAppMessage *)inAppMessage { if (self = [super init]) { _inAppMessage = inAppMessage; _isiPad = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad; return self; } else { return nil; } } #pragma mark - Lifecycle Methods - (void)viewDidLoad { [super viewDidLoad]; [ABKUIUtils enableAdjustsFontForContentSizeCategory:self.inAppMessageMessageLabel]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [ABKInAppMessageView class]; // Set colors of the IAM view at display time self.inAppMessageMessageLabel.text = self.inAppMessage.message; self.inAppMessageMessageLabel.textAlignment = self.inAppMessage.messageTextAlignment; if (self.inAppMessage.backgroundColor != nil) { self.view.backgroundColor = self.inAppMessage.backgroundColor; } if (self.inAppMessage.textColor != nil) { [self.inAppMessageMessageLabel setTextColor:self.inAppMessage.textColor]; } self.iconImageView.contentMode = self.inAppMessage.imageContentMode; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, self.inAppMessageMessageLabel); } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil); [[ABKUIUtils activeApplicationViewController] setNeedsStatusBarAppearanceUpdate]; } - (BOOL)prefersStatusBarHidden { return ABKUIUtils.applicationStatusBarHidden; } - (UIStatusBarStyle)preferredStatusBarStyle { return ABKUIUtils.applicationStatusBarStyle; } #pragma mark - UIViewController Methods // Inherit the supported orientations from the currently active application view // controller (the one immediately under the in-app message window) - (UIInterfaceOrientationMask)supportedInterfaceOrientations { return ABKUIUtils.activeApplicationViewController.supportedInterfaceOrientations; } #pragma mark - In-app Message Initialization - (BOOL)applyIconToLabelView:(UILabel *)iconLabelView { if ([ABKUIUtils objectIsValidAndNotEmpty:self.inAppMessage.icon]) { // Check if font awesome is already registered in the application. If not, register it. // The size can be any number here. if ([UIFont fontWithName:FontAwesomeName size:30] == nil) { NSString *fontPath = [[ABKUIUtils bundle:[ABKInAppMessageViewController class] channel:ABKInAppMessageChannel] pathForResource:FontAwesomeName ofType:@"otf"]; NSData *fontData = [NSData dataWithContentsOfFile:fontPath]; CFErrorRef error; CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)fontData); CGFontRef font = CGFontCreateWithDataProvider(provider); BOOL failedToRegisterFont = NO; if (!CTFontManagerRegisterGraphicsFont(font, &error)) { CFStringRef errorDescription = CFErrorCopyDescription(error); NSLog(@"Error: Cannot load Font Awesome"); CFBridgingRelease(errorDescription); failedToRegisterFont = YES; } CFRelease(font); CFRelease(provider); if (failedToRegisterFont) { return NO; } } iconLabelView.font = [UIFont fontWithName:FontAwesomeName size:self.iconLabelView.font.pointSize]; // The icon here is a Unicode string, so we use a text label instead of an image view iconLabelView.text = self.inAppMessage.icon; iconLabelView.textColor = self.inAppMessage.iconColor == nil ? [UIColor whiteColor] : self.inAppMessage.iconColor; iconLabelView.backgroundColor = self.inAppMessage.iconBackgroundColor == nil ? [UIColor colorWithRed:RedValueOfDefaultIconColorAndButtonBgColor green:GreenValueOfDefaultIconColorAndButtonBgColor blue:BlueValueOfDefaultIconColorAndButtonBgColor alpha:AlphaValueOfDefaultIconColorAndButtonBgColor] : self.inAppMessage.iconBackgroundColor; iconLabelView.layer.cornerRadius = self.isiPad ? InAppMessageIconLabelCornerRadius_iPad : InAppMessageIconLabelCornerRadius_iPhone; iconLabelView.layer.masksToBounds = YES; return YES; } return NO; } // Here we try to find the icon image and set it to the given image view. We will first try to find if the icon image // is one of the default Braze icon images. If not, we try to check the icon URI and download the image // asynchronously. // This method returns YES if we can find a default icon image, or there is a valid icon image URL. It returns NO when // we cannot find any icon from the in-app message, and won't do anything to the given image view. - (BOOL)applyImageToImageView:(UIImageView *)iconImageView { if ([ABKUIUtils objectIsValidAndNotEmpty:self.inAppMessage.imageURI]) { if ([Appboy sharedInstance].imageDelegate) { [[Appboy sharedInstance].imageDelegate setImageForView:iconImageView showActivityIndicator:NO withURL:self.inAppMessage.imageURI imagePlaceHolder:nil completed:nil]; return YES; } else { [self hideInAppMessage:NO]; return NO; } } return NO; } #pragma mark - Animation - (void)hideInAppMessage:(BOOL)animated { ABKInAppMessageWindowController *parentInAppMessageWindowController = (ABKInAppMessageWindowController *)self.parentViewController; [parentInAppMessageWindowController hideInAppMessageViewWithAnimation:animated]; } - (void)beforeMoveInAppMessageViewOnScreen {} - (void)moveInAppMessageViewOnScreen {} - (void)beforeMoveInAppMessageViewOffScreen {} - (void)moveInAppMessageViewOffScreen {} @end ================================================ FILE: AppboyUI/ABKInAppMessage/ViewControllers/ABKInAppMessageWindowController.h ================================================ #import #import "Appboy.h" #import "ABKInAppMessageUIDelegate.h" #import "ABKInAppMessageWindowController.h" #import "ABKInAppMessageWindow.h" NS_ASSUME_NONNULL_BEGIN // This notification is used to let the InAppMessageUIController know that the InAppMessageWindowController // was dismissed. static NSString * const ABKNotificationInAppMessageWindowDismissed = @"inAppMessageWindowDismissedNotification"; static double const InAppMessageAnimationDuration = 0.4; /*! * ABKInAppMessageWindowController is the view controller responsible for housing and displaying * ABKInAppMessageViewControllers and performing actions after the in-app message is clicked. Instances * of ABKInAppMessageWindowController are deallocated after the in-app message is dismissed. * * It will display the given in-app message view controller by animating it onto the screen, and * dismiss it by animating it off the screen, by calling the ABKInAppMessageViewController's * moveInAppMessageViewOffScreen: and moveInAppMessageViewOnScreen: methods, and log an impression * of the in-app message. * If the in-app message view controller is an instance of ABKInAppMessageSlideupViewController, * ABKInAppMessageModalViewController, or ABKInAppMessageFullViewController, it'll also handle the * following behaviors: * * For ABKInAppMessageSlideupViewController: * * set the width of the view controller based on slideup UI style and iPhone devices. * * add a tap gesture recognizer to the in-app message view controller, and handle the clicks on it. * * add a pan gesture recognizer to the in-app message view controller, and handle the panning on it. * * For ABKInAppMessageModalViewController: * * set the background color to be black with alpha 0.9. * * move the in-app view controller to the center. * * when the in-ap message has no buttons, add a tap gesture recognizer to the in-app message * view controller, and handle the clicks on it. * * block the clicks outside of the in-app message view. * * For ABKInAppMessageFullViewController: * * set the in-app message's frame to be full screen. * * when the in-app message has no buttons, add a tap gesture recognizer to the in-app message * view controller, and handle the clicks on it. * * Additionally, the view controller is responsible for executing that in-app message's specified * behavior on click or performing a "custom action", which can be specified through a delegate for * the in-app message. * * After the in-app message is dismissed, ABKInAppMessageWindowController will set the inAppMessageWindow * property to nil, and inform ABKInAppMessageUIController to set it's windowController property to * nil as well. At that point, the in-app message window's retainer count will drop to 0 and the * system will clean it out from the UIApplication's windows array. */ @interface ABKInAppMessageWindowController : UIViewController /*! * The UI window used to display the in-app message. */ @property (nonatomic, nullable) IBOutlet ABKInAppMessageWindow *inAppMessageWindow; /*! * The timer used to know when to slide the in-app message off the screen. */ @property (nullable) NSTimer *slideAwayTimer; /*! * The in-app message that is being displayed. */ @property ABKInAppMessage *inAppMessage; /*! * The optional ABKInAppMessageUIDelegate that can be used to customize display and behavior of the * in-app message. */ @property (weak, nullable) id inAppMessageUIDelegate; /*! * The view controller used to display the in-app message. */ @property ABKInAppMessageViewController *inAppMessageViewController; /*! * Properties used to properly place the slideup in-app messages with pan gestures. */ @property CGFloat slideupConstraintMaxValue; @property CGPoint inAppMessagePreviousPanPosition; /*! * The orientation mask that the in-app message supports. * The default value is UIInterfaceOrientationMaskAll */ @property UIInterfaceOrientationMask supportedOrientationMask; /*! * The preferred orientation for in-app message display. * The default is unknown, which means the orientation would be set as Status Bar current orientation. */ @property UIInterfaceOrientation preferredOrientation; /*! * The variable that shows if the device is being rotated. */ @property BOOL isInRotation; /*! * The variable that shows if the in-app message has been clicked. */ @property BOOL inAppMessageIsTapped; /*! * The ID of a button that has been clicked. */ @property NSInteger clickedButtonId; /*! * The ID of an HTML button that has been clicked. */ @property (nullable) NSString *clickedHTMLButtonId; - (instancetype)initWithInAppMessage:(ABKInAppMessage *)inAppMessage inAppMessageViewController:(ABKInAppMessageViewController *)inAppMessageViewController inAppMessageDelegate:(id)delegate; /*! * @discussion This method is called when the keyboard is shown when an in-app message is being displayed. * * For customization, please use a subclass or category to override this method. */ - (void)keyboardWasShown; /*! * @discussion This method is called to display the in-app message. * * For customization, please use a subclass or category to override this method. */ - (void)displayInAppMessageViewWithAnimation:(BOOL)withAnimation; /*! * @discussion These methods are called to hide the in-app message. * * For customization, please use a subclass or category to override one of these methods. */ - (void)hideInAppMessageViewWithAnimation:(BOOL)withAnimation; - (void)hideInAppMessageViewWithAnimation:(BOOL)withAnimation completionHandler:(void (^ __nullable)(void))completionHandler; /*! * @discussion This method is called when an in-app message button is clicked. * * For customization, please use a subclass or category to override this method. */ - (void)inAppMessageClickedWithActionType:(ABKInAppMessageClickActionType)actionType URL:(nullable NSURL *)url openURLInWebView:(BOOL)openUrlInWebView; NS_ASSUME_NONNULL_END @end ================================================ FILE: AppboyUI/ABKInAppMessage/ViewControllers/ABKInAppMessageWindowController.m ================================================ #import "ABKInAppMessageWindowController.h" #import "ABKInAppMessageWindow.h" #import "ABKInAppMessageView.h" #import "ABKInAppMessageModal.h" #import "ABKInAppMessageFull.h" #import "ABKInAppMessageHTMLFull.h" #import "ABKInAppMessageHTML.h" #import "ABKInAppMessageHTMLBase.h" #import "ABKInAppMessageHTMLBaseViewController.h" #import "ABKInAppMessageImmersiveViewController.h" #import "ABKInAppMessageSlideupViewController.h" #import "ABKInAppMessageModalViewController.h" #import "ABKInAppMessageViewController.h" #import "ABKURLDelegate.h" #import "ABKUIURLUtils.h" #import "ABKUIUtils.h" static CGFloat const MinimumInAppMessageDismissVelocity = 20.0; static CGFloat const SlideUpDragResistanceFactor = 0.055; static NSInteger const KeyWindowRetryMaxCount = 10; @interface ABKInAppMessageWindowController () @property (nonatomic, assign) NSInteger keyWindowRetryCount; @property (nonatomic, assign) BOOL isRemovingWindow; @end @implementation ABKInAppMessageWindowController - (instancetype)initWithInAppMessage:(ABKInAppMessage *)inAppMessage inAppMessageViewController:(ABKInAppMessageViewController *)inAppMessageViewController inAppMessageDelegate:(id)delegate { if (self = [super init]) { _inAppMessage = inAppMessage; _inAppMessageViewController = inAppMessageViewController; _inAppMessageUIDelegate = (id)delegate; _inAppMessageWindow = [self createInAppMessageWindow]; _inAppMessageWindow.backgroundColor = [UIColor clearColor]; _inAppMessageWindow.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin; _inAppMessageIsTapped = NO; _clickedButtonId = -1; _keyWindowRetryCount = 0; _isRemovingWindow = NO; } return self; } #pragma mark - Lifecycle Methods - (void)viewDidLoad { [super viewDidLoad]; [self addChildViewController:self.inAppMessageViewController]; [self.inAppMessageViewController didMoveToParentViewController:self]; self.view.backgroundColor = [UIColor clearColor]; if ([self.inAppMessage isKindOfClass:[ABKInAppMessageSlideup class]]) { // Note: this gestureRecognizer won't catch taps which occur during the animation. UITapGestureRecognizer *inAppSlideupTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(inAppMessageTapped:)]; [self.inAppMessageViewController.view addGestureRecognizer:inAppSlideupTapGesture]; UIPanGestureRecognizer *inAppSlideupPanGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(inAppSlideupWasPanned:)]; [self.inAppMessageViewController.view addGestureRecognizer:inAppSlideupPanGesture]; // We want to detect the pan gesture first, so we only recognize a tap when the pan recognizer fails. [inAppSlideupTapGesture requireGestureRecognizerToFail:inAppSlideupPanGesture]; } else if ([self.inAppMessage isKindOfClass:[ABKInAppMessageImmersive class]]) { UITapGestureRecognizer *inAppImmersiveInsideTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(inAppMessageTapped:)]; [self.inAppMessageViewController.view addGestureRecognizer:inAppImmersiveInsideTapGesture]; if ([self.inAppMessage isKindOfClass:[ABKInAppMessageModal class]]) { self.inAppMessageWindow.handleAllTouchEvents = YES; UITapGestureRecognizer *inAppModalOutsideTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(inAppMessageTappedOutside:)]; [self.view addGestureRecognizer:inAppModalOutsideTapGesture]; } } if ([self.inAppMessageViewController isKindOfClass:[ABKInAppMessageImmersiveViewController class]] || [self.inAppMessageViewController isKindOfClass:[ABKInAppMessageHTMLBaseViewController class]]) { self.inAppMessageWindow.accessibilityViewIsModal = YES; } [self.view addSubview:self.inAppMessageViewController.view]; } - (UIViewController *)childViewControllerForStatusBarHidden { return self.inAppMessageViewController; } - (UIViewController *)childViewControllerForStatusBarStyle { return self.inAppMessageViewController; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // When the in-app message first become visible, monitor windows changes in the view hierarchy to // ensure that the in-app message stays visible. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleWindowDidBecomeKeyNotification:) name:UIWindowDidBecomeKeyNotification object:nil]; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIWindowDidBecomeKeyNotification object:nil]; } #pragma mark - Rotation - (UIInterfaceOrientationMask)supportedInterfaceOrientations { return self.supportedOrientationMask; } - (BOOL)shouldAutorotate { if ([UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPad && self.inAppMessage.orientation != ABKInAppMessageOrientationAny && !self.inAppMessageWindow.hidden) { return NO; } else { return YES; } } - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation { if (self.preferredOrientation != UIInterfaceOrientationUnknown) { return self.preferredOrientation; } return [ABKUIUtils getInterfaceOrientation]; } #pragma mark - Gesture Recognizers - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { return ![touch.view isKindOfClass:[ABKInAppMessageView class]]; } - (void)inAppSlideupWasPanned:(UIPanGestureRecognizer *)panGestureRecognizer { ABKInAppMessageSlideupViewController *slideupVC = (ABKInAppMessageSlideupViewController *)self.inAppMessageViewController; BOOL animatesFromTop = ((ABKInAppMessageSlideup *)self.inAppMessage).inAppMessageSlideupAnchor == ABKInAppMessageSlideupFromTop; CGFloat offset = [panGestureRecognizer translationInView:self.view].y; CGFloat velocity = [panGestureRecognizer velocityInView:self.view].y; switch (panGestureRecognizer.state) { case UIGestureRecognizerStateChanged: { if (animatesFromTop) { slideupVC.offset = offset <= 0 ? offset : (SlideUpDragResistanceFactor * offset); } else { slideupVC.offset = offset >= 0 ? offset : (SlideUpDragResistanceFactor * offset); } break; } case UIGestureRecognizerStateEnded: case UIGestureRecognizerStateCancelled: { // Reset position if ((animatesFromTop && slideupVC.offset > 0) || (!animatesFromTop && slideupVC.offset < 0) || (fabs(velocity) < MinimumInAppMessageDismissVelocity && fabs(offset) < 16)) { slideupVC.offset = 0; [UIView animateWithDuration:0.2 animations:^{ [self.view layoutIfNeeded]; }]; return; } // Dismiss [self invalidateSlideAwayTimer]; if ([self.inAppMessageUIDelegate respondsToSelector:@selector(onInAppMessageDismissed:)]) { [self.inAppMessageUIDelegate onInAppMessageDismissed:self.inAppMessage]; } [slideupVC beforeMoveInAppMessageViewOffScreen]; [UIView animateWithDuration:0.2 animations:^{ [slideupVC moveInAppMessageViewOffScreen]; } completion:^(BOOL finished) { if (finished) { [self hideInAppMessageWindow]; } }]; break; } default: break; } } - (void)inAppMessageTapped:(id)sender { if ([self.inAppMessage isKindOfClass:[ABKInAppMessageImmersive class]] && [ABKUIUtils objectIsValidAndNotEmpty:((ABKInAppMessageImmersive *)self.inAppMessage).buttons]) { return; } [self invalidateSlideAwayTimer]; self.inAppMessageIsTapped = YES; if (![self delegateHandlesInAppMessageClick]) { [self inAppMessageClickedWithActionType:self.inAppMessage.inAppMessageClickActionType URL:self.inAppMessage.uri openURLInWebView:self.inAppMessage.openUrlInWebView]; } } - (void)inAppMessageTappedOutside:(id)sender { if (![self.inAppMessage isKindOfClass:[ABKInAppMessageModal class]]) { return; } if ([self.inAppMessageViewController isKindOfClass:ABKInAppMessageModalViewController.class]) { ABKInAppMessageModalViewController *viewController = (ABKInAppMessageModalViewController *)self.inAppMessageViewController; if (viewController.enableDismissOnOutsideTap) { [viewController dismissInAppMessage:self.inAppMessage]; } } } #pragma mark - Timer - (void)invalidateSlideAwayTimer { if (self.slideAwayTimer != nil) { [self.slideAwayTimer invalidate]; self.slideAwayTimer = nil; } } - (void)inAppMessageTimerFired:(NSTimer *)timer { if ([self.inAppMessageUIDelegate respondsToSelector:@selector(onInAppMessageDismissed:)]) { [self.inAppMessageUIDelegate onInAppMessageDismissed:self.inAppMessage]; } [self hideInAppMessageViewWithAnimation:self.inAppMessage.animateOut]; } #pragma mark - Keyboard - (void)keyboardWasShown { if (![self.inAppMessageViewController isKindOfClass:[ABKInAppMessageHTMLBaseViewController class]] && !self.inAppMessageWindow.hidden) { // If the keyboard is shown while an in-app message is on the screen, we hide the in-app message [self hideInAppMessageWindow]; } } #pragma mark - Windows - (void)resetKeyWindowRetryCount { self.keyWindowRetryCount = 0; } /*! * React to windows changes in the view hierarchy. This is needed to ensure that the in-app message * stays visible in cases where the host app decides to display a window (possibly the app's main * window) over our in-app message. * * This method tries to make the in-app message window visible up to 10 times — debounced with a * 0.1s timeout. The in-app message is dismissed when reaching that value to prevent infinite loops * when another window in the view hierarchy has a similar behavior. * * e.g. Some clients have extra logic when bootstrapping their app that can lead to the app's main * window being made key and visible after a delay at startup. In the case of test in-app messages * delivered via push notifications, our in-app messages would be displayed before the host app * window being made key and visible. Soon after, the host app window takes over and hides our * in-app message. */ - (void)handleWindowDidBecomeKeyNotification:(NSNotification *)notification { UIWindow *window = notification.object; // Cancel debounced reset [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(resetKeyWindowRetryCount) object:nil]; // Skip if this in-app message is already removing the window if (self.isRemovingWindow) { return; } // Skip for any in-app message window if ([window isKindOfClass:[ABKInAppMessageWindow class]]) { return; } // Skip if the new key window is meant to be displayed above the in-app message (alert, sheet, // host app toast) if (window.windowLevel > UIWindowLevelNormal) { return; } // Dismiss in-app message if we can't guarantee its visibility. self.keyWindowRetryCount += 1; if (self.keyWindowRetryCount >= KeyWindowRetryMaxCount) { NSLog(@"Error: Failed to make in-app message window key and visible %ld times, dismissing the in-app message.", (long)self.keyWindowRetryCount); [self hideInAppMessageViewWithAnimation:YES]; return; } // Force in-app message window to be displayed [self.inAppMessageWindow makeKeyAndVisible]; // Debounced reset, use NSRunLoopCommonModes as NSDefaultRunLoopMode does not update during // scroll events. [self performSelector:@selector(resetKeyWindowRetryCount) withObject:nil afterDelay:0.1 inModes:@[NSRunLoopCommonModes]]; } #pragma mark - Display and Hide In-app Message - (void)displayInAppMessageViewWithAnimation:(BOOL)withAnimation { dispatch_async(dispatch_get_main_queue(), ^{ // Set the root view controller after the inAppMessagewindow becomes the key window so it gets the // correct window size during and after rotation. self.keyWindowRetryCount = 0; [self.inAppMessageWindow makeKeyWindow]; self.inAppMessageWindow.rootViewController = self; self.inAppMessageWindow.hidden = NO; if (self.inAppMessage.inAppMessageDismissType == ABKInAppMessageDismissAutomatically) { self.slideAwayTimer = [NSTimer scheduledTimerWithTimeInterval:self.inAppMessage.duration + InAppMessageAnimationDuration target:self selector:@selector(inAppMessageTimerFired:) userInfo:nil repeats:NO]; } [self.view layoutIfNeeded]; [self.inAppMessageViewController beforeMoveInAppMessageViewOnScreen]; if (withAnimation) { [UIView animateWithDuration:InAppMessageAnimationDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ [self.inAppMessageViewController moveInAppMessageViewOnScreen]; } completion:^(BOOL finished){ [self.inAppMessage logInAppMessageImpression]; }]; } else { [self.inAppMessageViewController moveInAppMessageViewOnScreen]; [self.inAppMessage logInAppMessageImpression]; } }); } - (void)hideInAppMessageViewWithAnimation:(BOOL)withAnimation { [self hideInAppMessageViewWithAnimation:withAnimation completionHandler:nil]; } - (void)hideInAppMessageViewWithAnimation:(BOOL)withAnimation completionHandler:(void (^ __nullable)(void))completionHandler { [self.slideAwayTimer invalidate]; self.slideAwayTimer = nil; [self.view layoutIfNeeded]; [self.inAppMessageViewController beforeMoveInAppMessageViewOffScreen]; if (withAnimation) { [UIView animateWithDuration:InAppMessageAnimationDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ [self.inAppMessageViewController moveInAppMessageViewOffScreen]; } completion:^(BOOL finished){ if (completionHandler) { completionHandler(); } [self hideInAppMessageWindow]; }]; } else { [self.inAppMessageViewController moveInAppMessageViewOffScreen]; [self hideInAppMessageWindow]; } } - (void)hideInAppMessageWindow { if (self.isRemovingWindow) { return; } self.isRemovingWindow = YES; [self.slideAwayTimer invalidate]; self.slideAwayTimer = nil; self.inAppMessageWindow.rootViewController = nil; if (@available(iOS 13.0, *)) { self.inAppMessageWindow.windowScene = nil; } self.inAppMessageWindow = nil; [[NSNotificationCenter defaultCenter] postNotificationName:ABKNotificationInAppMessageWindowDismissed object:self userInfo:nil]; if (self.clickedButtonId >= 0) { [(ABKInAppMessageImmersive *)self.inAppMessage logInAppMessageClickedWithButtonID:self.clickedButtonId]; } else if (self.inAppMessageIsTapped) { [self.inAppMessage logInAppMessageClicked]; } else if ([ABKUIUtils objectIsValidAndNotEmpty:self.clickedHTMLButtonId]) { [(ABKInAppMessageHTMLBase *)self.inAppMessage logInAppMessageHTMLClickWithButtonID:self.clickedHTMLButtonId]; } } #pragma mark - In-app Message and Button Clicks - (BOOL)delegateHandlesInAppMessageClick { if ([self.inAppMessageUIDelegate respondsToSelector:@selector(onInAppMessageClicked:)]) { if ([self.inAppMessageUIDelegate onInAppMessageClicked:self.inAppMessage]) { NSLog(@"No in-app message click action will be performed by Braze as inAppMessageDelegate %@ returned YES in onInAppMessageClicked:", self.inAppMessageUIDelegate); return YES; } } return NO; } - (void)inAppMessageClickedWithActionType:(ABKInAppMessageClickActionType)actionType URL:(NSURL *)url openURLInWebView:(BOOL)openUrlInWebView { [self invalidateSlideAwayTimer]; switch (actionType) { case ABKInAppMessageNoneClickAction: break; case ABKInAppMessageDisplayNewsFeed: [self displayModalFeedView]; break; case ABKInAppMessageRedirectToURI: if ([ABKUIUtils objectIsValidAndNotEmpty:url]) { [self handleInAppMessageURL:url inWebView:openUrlInWebView]; } break; } [self hideInAppMessageViewWithAnimation:self.inAppMessage.animateOut]; } #pragma mark - Display News Feed - (void)displayModalFeedView { Class ModalFeedViewControllerClass = [ABKUIUtils getModalFeedViewControllerClass]; if (ModalFeedViewControllerClass != nil) { UIViewController *topmostViewController = [ABKUIURLUtils topmostViewControllerWithRootViewController:ABKUIUtils.activeApplicationViewController]; [topmostViewController presentViewController:[[ModalFeedViewControllerClass alloc] init] animated:YES completion:nil]; } } #pragma mark - URL Handling - (void)handleInAppMessageURL:(NSURL *)url inWebView:(BOOL)openUrlInWebView { // URL Delegate if ([ABKUIURLUtils URLDelegate:Appboy.sharedInstance.appboyUrlDelegate handlesURL:url fromChannel:ABKInAppMessageChannel withExtras:self.inAppMessage.extras]) { return; } // WebView if ([ABKUIURLUtils URL:url shouldOpenInWebView:openUrlInWebView]) { UIViewController *topmostViewController = [ABKUIURLUtils topmostViewControllerWithRootViewController:ABKUIUtils.activeApplicationViewController]; [ABKUIURLUtils displayModalWebViewWithURL:url topmostViewController:topmostViewController]; return; } // System [ABKUIURLUtils openURLWithSystem:url]; } #pragma mark - Helpers /*! * Creates and setups the ABKInAppMessageWindow used to display the in-app message * * @discussion First tries to create the window with the current UIWindowScene if available, then fallbacks * to create the window with a frame. */ - (ABKInAppMessageWindow *)createInAppMessageWindow { ABKInAppMessageWindow *window; if (@available(iOS 13.0, *)) { UIWindowScene *windowScene = ABKUIUtils.activeWindowScene; if (windowScene) { window = [[ABKInAppMessageWindow alloc] initWithWindowScene:windowScene]; } } if (!window) { window = [[ABKInAppMessageWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; } window.backgroundColor = UIColor.clearColor; window.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; return window; } @end ================================================ FILE: AppboyUI/ABKNewsFeed/AppboyNewsFeed.h ================================================ // Braze News Feed View Controllers #import "ABKFeedWebViewController.h" #import "ABKNewsFeedTableViewController.h" #import "ABKNewsFeedViewController.h" // Braze News Feed Cells #import "ABKNFBannerCardCell.h" #import "ABKNFBaseCardCell.h" #import "ABKNFCaptionedMessageCardCell.h" #import "ABKNFClassicCardCell.h" ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/Base.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Done"; "Appboy.feed.no-card.text" = "We have no updates.\nPlease check again later."; "Appboy.feed.no-connection.title" = "Connection Error"; "Appboy.feed.no-connection.message" = "Cannot establish network connection.\nPlease try again later."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/ar.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "تم"; "Appboy.feed.no-card.text" = "ليس لدينا أي تحديث. يرجى التحقق مرة أخرى لاحقاً."; "Appboy.feed.no-connection.title" = "خلل في الاتصال"; "Appboy.feed.no-connection.message" = "لا يمكن إجراء الاتصال بالشبكة. يرجى تكرار المحاولة لاحقا. "; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/cs.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Hotovo"; "Appboy.feed.no-card.text" = "Nemáme žádné aktualizace.\nZkontrolujte prosím znovu později."; "Appboy.feed.no-connection.title" = "Chyba připojení"; "Appboy.feed.no-connection.message" = "Nelze navázat síťové připojení.\nProsím zkuste to znovu později."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/da.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Afsluttet"; "Appboy.feed.no-card.text" = "Vi har ingen updates. Prøv venligst senere"; "Appboy.feed.no-connection.title" = "Netværksfejl"; "Appboy.feed.no-connection.message" = "Kan ikke etablere netværksforbindelse. Prøv venligst senere."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/de.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Fertig"; "Appboy.feed.no-card.text" = "Derzeit sind keine Updates verfügbar.\nBitte später noch einmal versuchen."; "Appboy.feed.no-connection.title" = "Verbindungsfehler"; "Appboy.feed.no-connection.message" = "Netzwerkverbindung kann nicht aufgebaut werden.\nBitte später noch einmal versuchen."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/en.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Done"; "Appboy.feed.no-card.text" = "We have no updates.\nPlease check again later."; "Appboy.feed.no-connection.title" = "Connection Error"; "Appboy.feed.no-connection.message" = "Cannot establish network connection.\nPlease try again later."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/es-419.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Listo"; "Appboy.feed.no-card.text" = "No tenemos ninguna actualización. Vuelva a verificar más tarde."; "Appboy.feed.no-connection.title" = "Error de conexión"; "Appboy.feed.no-connection.message" = "No se puede establecer conexión con la red. Por favor, vuelva a intentarlo más tarde."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/es-MX.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Listo"; "Appboy.feed.no-card.text" = "No tenemos ninguna actualización. Vuelva a verificar más tarde."; "Appboy.feed.no-connection.title" = "Error de conexión"; "Appboy.feed.no-connection.message" = "No se puede establecer conexión con la red. Por favor, vuelva a intentarlo más tarde."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/es.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Finalizado"; "Appboy.feed.no-card.text" = "No tenemos actualizaciones. Por favor compruébelo más tarde."; "Appboy.feed.no-connection.title" = "Error de conexión"; "Appboy.feed.no-connection.message" = "No se puede establecer conexión de red. Por favor inténtelo más tarde."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/et.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Valmis"; "Appboy.feed.no-card.text" = "Uuendusi pole praegu saadaval. Proovige hiljem uuesti."; "Appboy.feed.no-connection.title" = "Üheduse viga"; "Appboy.feed.no-connection.message" = "Võrguühenduse loomine ebaõnnestus. Proovige hiljem uuesti."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/fi.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Valmis"; "Appboy.feed.no-card.text" = "Päivityksiä ei ole saatavilla. Tarkista myöhemmin uudelleen."; "Appboy.feed.no-connection.title" = "Yhteysvirhe"; "Appboy.feed.no-connection.message" = "Verkkoyhteyttä ei voida luoda. Yritä myöhemmin uudelleen."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/fil.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Gawa na"; "Appboy.feed.no-card.text" = "Wala kaming mga update. Mangyaring suriin muli sa ibang pagkakataon."; "Appboy.feed.no-connection.title" = "May Error sa Koneksyon"; "Appboy.feed.no-connection.message" = "Hindi makapagtatag ng koneksyon sa network. Mangyaring subukan muli mamaya."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/fr.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Fini"; "Appboy.feed.no-card.text" = "Aucune mise à jour disponible. Veuillez vérifier ultérieurement."; "Appboy.feed.no-connection.title" = "Erreur de connexion."; "Appboy.feed.no-connection.message" = "Impossible d'établir la connexion réseau. Veuillez réessayer ultérieurement."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/he.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "סיום"; "Appboy.feed.no-card.text" = ".אין לנו עדכונים. בבקשה בדוק שוב בקרוב"; "Appboy.feed.no-connection.title" = "שגיאת חיבור רשת"; "Appboy.feed.no-connection.message" = ".לא ניתן לקבוע חיבור רשת. בבקשה נסה שוב בקרוב"; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/hi.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "कर दिया गया"; "Appboy.feed.no-card.text" = "हमारे पास कोई अपडेट नहीं हैं। कृपया बाद में फिर से जाँच करें.।"; "Appboy.feed.no-connection.title" = "कनेक्शन की त्रुटि"; "Appboy.feed.no-connection.message" = "नेटवर्क कनेक्शन स्थापित नहीं हो रहा है। कृपया बाद में दोबारा प्रयास करें।."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/id.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Selesai"; "Appboy.feed.no-card.text" = "Kami tidak memiliki pembaruan. Coba lagi nanti."; "Appboy.feed.no-connection.title" = "Kesalahan Koneksi"; "Appboy.feed.no-connection.message" = "Tidak bisa melakukan koneksi jaringan. Coba lagi nanti."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/it.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Fatto"; "Appboy.feed.no-card.text" = "Non ci sono aggiornamenti. Ricontrollare più tardi."; "Appboy.feed.no-connection.title" = "Errore di connessione"; "Appboy.feed.no-connection.message" = "Impossibile stabilire una connessione di rete. Riprovare più tardi."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/ja.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "完了"; "Appboy.feed.no-card.text" = "アップデートはありません。後でもう一度確認してください。"; "Appboy.feed.no-connection.title" = "接続エラー"; "Appboy.feed.no-connection.message" = "ネットワークに接続できません。後でもう一度試してください。"; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/km.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "បានសម្រេច"; "Appboy.feed.no-card.text" = "យើងមិនមានការធ្វើបច្ចុប្បន្នភាពទេ។ សូមពិនិត្យមើលម្តងទៀតនៅពេលក្រោយ."; "Appboy.feed.no-connection.title" = "កំហុសឆ្គងក្នុងការតភ្ជាប់"; "Appboy.feed.no-connection.message" = "មិនអាចបង្កើតបណ្តាញតភ្ជាប់បានទេ។ សូមព្យាយាមម្តងទៀតនៅពេលក្រោយ."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/ko.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "완료 "; "Appboy.feed.no-card.text" = "업데이트가 없습니다. 다음에 다시 확인해 주십시오."; "Appboy.feed.no-connection.title" = "연결 오류"; "Appboy.feed.no-connection.message" = "네트워크 연결을 할 수 없습니다. 나중에 다시 시도해 주십시오."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/lo.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "ສຳ​ເລັດ"; "Appboy.feed.no-card.text" = "ພວກ​ເຮົາ​ບໍ່​ມີ​ການ​ອັບ​ເດດ. ກະ​ລຸ​ນາ​ລອງ​ໃໝ່​ພາຍ​ຫຼັງ."; "Appboy.feed.no-connection.title" = "ການ​ເຊື່ອມ​ຕໍ່​ຜິດ​ພາດ"; "Appboy.feed.no-connection.message" = "ບໍ່​ສາ​ມາດ​ຕັ້ງ​ການ​ເຊື່ອມ​ຕໍ່​ເຄືອ​ຂ່າຍ​ໄດ້. ກະ​ລຸ​ນາ​ລອງ​ໃໝ່​ພາຍ​ຫຼັງ."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/ms.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Selesai"; "Appboy.feed.no-card.text" = "Tiada kemas kini. Sila periksa kemudian."; "Appboy.feed.no-connection.title" = "Ralat Sambungan"; "Appboy.feed.no-connection.message" = "Tidak boleh membuat sambungan rangkaian. Sila cuba kemudian."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/my.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "ျပီးျပီ"; "Appboy.feed.no-card.text" = "ကၽႊႏု္ပ္ တို႕တြင္ အသစ္တင္ျပရန္မရွိပါ။ ေက်းဇူးျပဳ၍ ေနာင္တြင္ ထပ္စစ္ပါ။ ."; "Appboy.feed.no-connection.title" = "ဆက္သြယ္ေရး အမွား"; "Appboy.feed.no-connection.message" = "ကြန္ယက္ဆက္သြယ္ျခင္း မျပဳလုပ္ႏိုင္ပါ။ ေက်းဇူးျပဳ၍ ထပ္မံၾကိဳးစားၾကည္႕ပါ။."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/nb.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Ferdig"; "Appboy.feed.no-card.text" = "Vi har ingen oppdateringer. Vennligst sjekk igjen senere."; "Appboy.feed.no-connection.title" = "Tilkoblingsfeil"; "Appboy.feed.no-connection.message" = "Kan ikke etablere nettverkstilkobling. Vennligst prøv igjen senere."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/nl.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Gereed"; "Appboy.feed.no-card.text" = "Er zijn geen updates. Probeer het later opnieuw."; "Appboy.feed.no-connection.title" = "Verbindingsfout"; "Appboy.feed.no-connection.message" = "Kan geen netwerkverbinding maken. Probeer het later opnieuw."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/pl.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Gotowe"; "Appboy.feed.no-card.text" = "Brak aktualizacji. Proszę sprawdzić ponownie później."; "Appboy.feed.no-connection.title" = "Błąd połączenia"; "Appboy.feed.no-connection.message" = "Nie można ustanowić połączenia z siecią. Proszę spróbować ponownie później."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/pt-PT.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Concluído"; "Appboy.feed.no-card.text" = "Não temos atualizações. Por favor, verifique mais tarde."; "Appboy.feed.no-connection.title" = "Erro de Ligação"; "Appboy.feed.no-connection.message" = "Não é possível estabelecer a ligação à rede. Por favor, tente mais tarde."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/pt.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Concluído"; "Appboy.feed.no-card.text" = "Não temos nenhuma atualização.\nVerifique novamente mais tarde."; "Appboy.feed.no-connection.title" = "Erro de conexão"; "Appboy.feed.no-connection.message" = "Não foi possível estabelecer uma\nconexão. Tente novamente mais tarde."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/ru.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Готово"; "Appboy.feed.no-card.text" = "Обновления недоступны. Пожалуйста, проверьте снова позже."; "Appboy.feed.no-connection.title" = "Ошибка подключения"; "Appboy.feed.no-connection.message" = "Невозможно установить сетевое подключение. Пожалуйста, повторите попытку позже."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/sv.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Klar"; "Appboy.feed.no-card.text" = "Det finns inga uppdateringar. Försök igen senare."; "Appboy.feed.no-connection.title" = "Anslutningsfel"; "Appboy.feed.no-connection.message" = "Det gick inte att skapa en nätverksanslutning. Försök igen senare."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/th.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "เสร็จสิ้น"; "Appboy.feed.no-card.text" = "เราไม่มีการอัพเดต กรุณาตรวจสอบภายหลัง."; "Appboy.feed.no-connection.title" = "ผิดพลาดการเชื่อมต่อ"; "Appboy.feed.no-connection.message" = "ไม่สามารถสร้างการเชื่อมต่อเครือข่าย กรุณาลองใหม่ภายหลัง."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/uk.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Готово"; "Appboy.feed.no-card.text" = "Оновлення недоступні.\nБудь ласка, перевірте знову пізніше."; "Appboy.feed.no-connection.title" = "Помилка підключення"; "Appboy.feed.no-connection.message" = "неможливо встановити з'єднання з мережею.\nБудь ласка, спробуйте ще раз пізніше."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/vi.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "Hoàn tất"; "Appboy.feed.no-card.text" = "Chúng tôi không có cập nhật nào. Vui lòng kiểm tra lại sau."; "Appboy.feed.no-connection.title" = "Lỗi Kết Nối"; "Appboy.feed.no-connection.message" = "Không thể thiết lập kết nối mạng. Vui lòng thử lại sau."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/zh-HK.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "完成"; "Appboy.feed.no-card.text" = "暫時沒有更新.\n請稍候再試."; "Appboy.feed.no-connection.title" = "連線錯誤"; "Appboy.feed.no-connection.message" = "無法建立網路連線.\n請稍候再試."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/zh-Hans.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "完成"; "Appboy.feed.no-card.text" = "暂时没有更新.\n请稍后再试."; "Appboy.feed.no-connection.title" = "连接错误"; "Appboy.feed.no-connection.message" = "无法建立网络连接.\n请稍候再试."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/zh-Hant.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "完成"; "Appboy.feed.no-card.text" = "暫時沒有更新.\n請稍候再試."; "Appboy.feed.no-connection.title" = "連線錯誤"; "Appboy.feed.no-connection.message" = "無法建立網路連線.\n請稍候再試."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/zh-TW.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "完成"; "Appboy.feed.no-card.text" = "暫時沒有更新.\n請稍候再試."; "Appboy.feed.no-connection.title" = "連線錯誤"; "Appboy.feed.no-connection.message" = "無法建立網路連線.\n請稍候再試."; ================================================ FILE: AppboyUI/ABKNewsFeed/Resources/zh.lproj/AppboyFeedLocalizable.strings ================================================ /* News Feed Context Labels */ "Appboy.feed.done-button.title" = "完成"; "Appboy.feed.no-card.text" = "暂时没有更新.\n请稍后再试."; "Appboy.feed.no-connection.title" = "连接错误"; "Appboy.feed.no-connection.message" = "无法建立网络连接.\n请稍候再试."; ================================================ FILE: AppboyUI/ABKNewsFeed/ViewControllers/ABKFeedWebViewController.h ================================================ #import #import @interface ABKFeedWebViewController : UIViewController /*! * The URL the modal web view controller should open. Please note that this is the initial URL and * won't be updated if the initial URL re-directs to another URL. */ @property NSURL *url; /*! * The WKWebView which displays the web page. */ @property (nonatomic) IBOutlet WKWebView *webView; /*! * The UIProgressView which shows the web view loading process. It will be on top of the web view and * will disappear as soon as the page is loaded. */ @property (nonatomic) IBOutlet UIProgressView *progressBar; /*! * The property tells the web view controller to add a Done button or not. The default value is NO. * Please set this property before displaying the web view controller. */ @property (nonatomic) BOOL showDoneButton; @end ================================================ FILE: AppboyUI/ABKNewsFeed/ViewControllers/ABKFeedWebViewController.m ================================================ #import "ABKFeedWebViewController.h" #import "ABKNoConnectionLocalization.h" #import "ABKUIUtils.h" static NSString *const EstimatedProgressKeyPath = @"estimatedProgress"; static NSString *const LocalizedNoConnectionKey = @"Appboy.no-connection.message"; @implementation ABKFeedWebViewController - (void)viewDidLoad { [super viewDidLoad]; self.webView.navigationDelegate = self; self.webView = [self getWebView]; self.view = self.webView; #if !TARGET_OS_TV if (@available(iOS 15.0, *)) { self.view.backgroundColor = UIColor.systemGroupedBackgroundColor; } #endif [self setupProgressBar]; if (self.showDoneButton) { UIBarButtonItem *closeBarButton = [self getDoneBarButtonItem]; [self.navigationItem setRightBarButtonItem:closeBarButton]; } [self.webView addObserver:self forKeyPath:EstimatedProgressKeyPath options:NSKeyValueObservingOptionNew context:nil]; [self.webView loadRequest:[NSURLRequest requestWithURL:self.url]]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([ABKUIUtils string:EstimatedProgressKeyPath isEqualToString:keyPath]) { if (self.webView.estimatedProgress == 1.0) { [UIView animateWithDuration:1 animations:^{ self.progressBar.alpha = 0.0; }]; } else if (self.webView.estimatedProgress < 1.0) { self.progressBar.alpha = 1.0; [self.progressBar setProgress:self.webView.estimatedProgress animated:YES]; } } } - (void)dealloc { [self.webView removeObserver:self forKeyPath:EstimatedProgressKeyPath]; } #pragma mark - Customization Methods /*! * @discussion Returns a WKWebView object, whose navigationDelegate is this ABKFeedWebViewController instance. * * If you want to do any customization to the WKWebView, please override this method in an ABKFeedWebViewController * category and return the customized WKWebView. All instances of ABKFeedWebViewController will then * call the category's `getWebView` implementation instead of this method. * */ - (WKWebView *)getWebView { WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero]; webView.navigationDelegate = self; return webView; } /*! * * @discussion Creates a UIProgressView and puts it on top of the web view. * * If you want to do any customization to the progress bar, please override this method in an ABKFeedWebViewController * category and set up the progress bar. All instances of ABKFeedWebViewController will then * call the category's `setupProgressBar:` implementation instead of this method. * */ - (void)setupProgressBar{ UIProgressView *progressBar = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleBar]; progressBar.alpha = 0; self.progressBar = progressBar; [self.view addSubview:self.progressBar]; self.progressBar.translatesAutoresizingMaskIntoConstraints = NO; [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.progressBar attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.topLayoutGuide attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[progressBar]|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:@{@"progressBar" : self.progressBar}]]; } /*! * @discussion Returns the Done UIBarButtonItem, which allows the user to dismiss the modal web view. * * If you want to do any customization to the Done button, please override this method in an ABKFeedWebViewController * category and return the customized UIBarButtonItem. All instances of ABKFeedWebViewController will then * call the category's `getDoneBarButtonItem` implementation instead of this method. * */ - (UIBarButtonItem *)getDoneBarButtonItem { return [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(closeButtonPressed:)]; } - (void)closeButtonPressed:(id)sender { [self dismissViewControllerAnimated:YES completion:nil]; } #pragma mark - WKNavigationDelegate methods - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { NSString *urlString = [[navigationAction.request.mainDocumentURL absoluteString] lowercaseString]; NSArray *stringComponents = [urlString componentsSeparatedByString:@":"]; if ([stringComponents[1] hasPrefix:@"//itunes.apple.com"] || (![stringComponents[0] isEqual:@"http"] && ![stringComponents[0] isEqual:@"https"])) { // Dismiss the modal web view and let the system handle the deep links if ([[UIApplication sharedApplication] openURL:navigationAction.request.URL]) { decisionHandler(WKNavigationActionPolicyCancel); [self.navigationController popViewControllerAnimated:NO]; return; } } decisionHandler(WKNavigationActionPolicyAllow); } - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { self.progressBar.alpha = 0.0; } - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error { self.progressBar.alpha = 0.0; // Display localized "No Connection" message UILabel *label = [[UILabel alloc] init]; label.textAlignment = NSTextAlignmentCenter; label.numberOfLines = 0; NSString *localizedNoConectionMessage = NSLocalizedString(@"Appboy.no-connection.message", @"No connection error message for URL loading failure"); if (localizedNoConectionMessage.length == 0 || [ABKUIUtils string:LocalizedNoConnectionKey isEqualToString:localizedNoConectionMessage]) { localizedNoConectionMessage = [ABKNoConnectionLocalization getNoConnectionLocalizedString]; } label.text = localizedNoConectionMessage; [self.webView addSubview:label]; label.translatesAutoresizingMaskIntoConstraints = NO; [self.webView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-10-[noConnectionLabel]-10-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:@{@"noConnectionLabel" : label}]]; [self.webView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[noConnectionLabel]|" options:NSLayoutFormatAlignAllCenterY metrics:nil views:@{@"noConnectionLabel" : label}]]; } @end ================================================ FILE: AppboyUI/ABKNewsFeed/ViewControllers/ABKNewsFeedTableViewController.h ================================================ #import #import "AppboyKit.h" #import "ABKNFBaseCardCell.h" @interface ABKNewsFeedTableViewController : UITableViewController /*! * @discussion Initialization that is done for all ABKNewsFeedTableViewControllers with or without storyboard/XIB. */ - (void)setUp; /*! * @discussion Initialization that is done for ABKNewsFeedTableViewControllers with programmatic layout only. */ - (void)setUpUI; /*! * @discussion Registers Cell classes with the tableview, override this method when implementing custom * cell classes to register the new subclasses. */ - (void)registerTableViewCellClasses; /*! * @param tableView The table view which need the cell to diplay the card UI. * @param indexPath The index path of the card UI in the table view. * @param card The card model for the cell. * * @discussion This method dequeues and returns the corresponding card cell based on card type from * the given table view. */ - (ABKNFBaseCardCell *)dequeueCellFromTableView:(UITableView *)tableView forIndexPath:(NSIndexPath *)indexPath forCard:(ABKCard *)card; /*! * UI elements which are used in the News Feed table view. You can find them in the News Feed Card Storyboard. */ @property (nonatomic) IBOutlet UIView *emptyFeedView; @property (nonatomic) IBOutlet UILabel *emptyFeedLabel; /*! * This property allows you to enable or disable the unread indicator on the news feed. The default * value is NO, which will enable the displaying of the unread indicator on cards. */ @property (nonatomic) BOOL disableUnreadIndicator; /*! * This property indicates which categories of cards the news feed is displaying. * Setting this property will automatically update the news feed page and only display cards in the given categories. * This method won't request refresh of cards from the Braze server, but only look into cards that are cached in the SDK. */ @property (nonatomic) ABKCardCategory categories; /*! * This property shows the cards displayed in the News Feed. Please note that the News Feed view * controller listens to the ABKFeedUpdatedNotification notification from the Braze SDK, which will * update the value of this property. */ @property (nonatomic) NSArray *cards; /*! * This set stores the card IDs for which the impressions have been logged. */ @property (nonatomic) NSMutableSet *cardImpressions; /*! * This property defines the timeout for stored News Feed cards in the Braze SDK. If the cards in the * Braze SDK are older than this value, the News Feed view controller will request a News Feed update. * * The default value is 60 seconds. */ @property NSTimeInterval cacheTimeout; @property id constraintWarningValue; /*! * @discussion This method returns an instance of ABKNewsFeedTableViewController. You can call it * to get a News Feed view controller for your navigation controller. * @warning To use a custom News Feed view controller, instantiate your own subclass instead * (e.g. via alloc / init). */ + (instancetype)getNavigationFeedViewController; /*! * @discussion Given a content card return the type identifier for the above * registration. */ - (NSString *)findCellIdentifierWithCard:(ABKCard *)card; /*! * @discussion This method returns the localized string from AppboyFeedLocalizable.strings file. * You can easily override the localized string by adding the keys and the translations to your own * Localizable.strings file. * * To do custom handling with the Appboy localized string, you can override this method in a * subclass. */ - (NSString *)localizedAppboyFeedString:(NSString *)key; /*! * @discussion This method handles the user's click on the card. * * To do custom handling with the card clicks, you can override this method in a * subclass. You also need to call [card logCardClicked] manually inside of your new method * to send the click event to the Braze server. */ - (void)handleCardClick:(ABKCard *)card; @end ================================================ FILE: AppboyUI/ABKNewsFeed/ViewControllers/ABKNewsFeedTableViewController.m ================================================ #import "ABKNewsFeedTableViewController.h" #import "ABKNFBannerCardCell.h" #import "ABKNFCaptionedMessageCardCell.h" #import "ABKNFClassicCardCell.h" #import "ABKUIUtils.h" #import "ABKFeedWebViewController.h" #import "ABKUIURLUtils.h" @implementation ABKNewsFeedTableViewController #pragma mark - Initialization - (instancetype)init { self = [super init]; if (self) { [self setUp]; [self setUpUI]; [self registerTableViewCellClasses]; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self setUp]; } return self; } #pragma mark - SetUp - (void)setUp { _categories = ABKCardCategoryAll; _cacheTimeout = 60.0; _cardImpressions = [NSMutableSet set]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(feedUpdated:) name:ABKFeedUpdatedNotification object:nil]; } - (void)setUpUI { #if !TARGET_OS_TV if (@available(iOS 15.0, *)) { self.view.backgroundColor = UIColor.systemGroupedBackgroundColor; } #endif self.emptyFeedView = [[UIView alloc] init]; self.emptyFeedView.translatesAutoresizingMaskIntoConstraints = NO; [self.view addSubview:self.emptyFeedView]; [self.emptyFeedView.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor].active = YES; [self.emptyFeedView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor].active = YES; self.emptyFeedLabel = [[UILabel alloc] init]; self.emptyFeedLabel.translatesAutoresizingMaskIntoConstraints = NO; self.emptyFeedLabel.text = [self localizedAppboyFeedString:@"Appboy.feed.no-card.text"]; [self.emptyFeedView addSubview:self.emptyFeedLabel]; [self.emptyFeedLabel.topAnchor constraintEqualToAnchor:self.emptyFeedView.topAnchor].active = YES; [self.emptyFeedLabel.bottomAnchor constraintEqualToAnchor:self.emptyFeedView.bottomAnchor].active = YES; [self.emptyFeedLabel.trailingAnchor constraintEqualToAnchor:self.emptyFeedView.trailingAnchor].active = YES; [self.emptyFeedLabel.leadingAnchor constraintEqualToAnchor:self.emptyFeedView.leadingAnchor].active = YES; self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; self.tableView.backgroundView = nil; if (@available(iOS 13.0, *)) { self.tableView.backgroundColor = [UIColor systemGroupedBackgroundColor]; } else { self.tableView.backgroundColor = [UIColor groupTableViewBackgroundColor]; } UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init]; [refreshControl addTarget:self action:@selector(refreshNewsFeed:) forControlEvents:UIControlEventValueChanged]; self.refreshControl = refreshControl; self.navigationItem.title = @"News Feed"; } # pragma mark - View Controller Life Cycle Methods - (void)viewDidLoad { [super viewDidLoad]; self.cards = [[Appboy sharedInstance].feedController getCardsInCategories:self.categories]; self.tableView.rowHeight = UITableViewAutomaticDimension; self.tableView.estimatedRowHeight = 160; [self requestNewCardsIfTimeout]; self.emptyFeedLabel.text = [self localizedAppboyFeedString:@"Appboy.feed.no-card.text"]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self updateAndDisplayCardsFromCache]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [[Appboy sharedInstance] logFeedDisplayed]; } - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator { [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; [coordinator animateAlongsideTransition:nil completion:^(id _Nonnull context) { [self.tableView reloadData]; }]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - Update And Display Cached Cards - (IBAction)refreshNewsFeed:(UIRefreshControl *)sender { [[Appboy sharedInstance] requestFeedRefresh]; } - (void)requestNewCardsIfTimeout { NSTimeInterval passedTime = fabs([[Appboy sharedInstance].feedController.lastUpdate timeIntervalSinceNow]); if (passedTime > self.cacheTimeout) { [[Appboy sharedInstance] requestFeedRefresh]; } } - (void)feedUpdated:(NSNotification *)notification { BOOL isSuccessful = [notification.userInfo[ABKFeedUpdatedIsSuccessfulKey] boolValue]; if (isSuccessful) { [self updateAndDisplayCardsFromCache]; } [self.refreshControl endRefreshing]; } - (void)updateAndDisplayCardsFromCache { self.cards = [[Appboy sharedInstance].feedController getCardsInCategories:self.categories]; if (self.cards == nil || self.cards.count == 0) { [self hideTableViewAndShowViewInHeader:self.emptyFeedView]; } else { [self showTableViewAndHideHeaderViews]; } [self.tableView reloadData]; } - (void)hideTableViewAndShowViewInHeader:(UIView *)view { view.hidden = NO; view.frame = self.view.bounds; [view layoutIfNeeded]; self.tableView.sectionHeaderHeight = self.tableView.frame.size.height; self.tableView.tableHeaderView = view; self.tableView.scrollEnabled = NO; } - (void)showTableViewAndHideHeaderViews { self.emptyFeedView.hidden = YES; self.tableView.tableHeaderView = nil; self.tableView.sectionHeaderHeight = 0; self.tableView.scrollEnabled = YES; } #pragma mark - Configuration Update - (void)setDisableUnreadIndicator:(BOOL)disableUnreadIndicator { if (disableUnreadIndicator != _disableUnreadIndicator) { _disableUnreadIndicator = disableUnreadIndicator; [self updateAndDisplayCardsFromCache]; } } - (void)setCategories:(ABKCardCategory)categories { if (categories != _categories) { _categories = categories; [self updateAndDisplayCardsFromCache]; } } #pragma mark - Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.cards.count; } - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { BOOL cellVisible = [[tableView indexPathsForVisibleRows] containsObject:indexPath]; if (cellVisible) { ABKCard *card = self.cards[indexPath.row]; [self logCardImpressionIfNeeded:card]; } } - (void)logCardImpressionIfNeeded:(ABKCard *)card { if ([self.cardImpressions containsObject:card.idString]) { // do nothing if we have already logged an impression return; } [card logCardImpression]; [self.cardImpressions addObject:card.idString]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { ABKCard *card = self.cards[indexPath.row]; ABKNFBaseCardCell *cell = [self dequeueCellFromTableView:tableView forIndexPath:indexPath forCard:card]; [cell applyCard:card]; cell.delegate = self; cell.hideUnreadIndicator = self.disableUnreadIndicator; return cell; } - (void)registerTableViewCellClasses { [self.tableView registerClass:[ABKNFBannerCardCell class] forCellReuseIdentifier:@"ABKBannerCardCell"]; [self.tableView registerClass:[ABKNFCaptionedMessageCardCell class] forCellReuseIdentifier:@"ABKNFCaptionedMessageCardCell"]; [self.tableView registerClass:[ABKNFClassicCardCell class] forCellReuseIdentifier:@"ABKNFNewsCardCell"]; } - (ABKNFBaseCardCell *)dequeueCellFromTableView:(UITableView *)tableView forIndexPath:(NSIndexPath *)indexPath forCard:(ABKCard *)card { NSString *cellIdentifier = [self findCellIdentifierWithCard:card]; return [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath]; } - (NSString *)findCellIdentifierWithCard:(ABKCard *)card { if ([card isKindOfClass:[ABKBannerCard class]]) { return @"ABKBannerCardCell"; } else if ([card isKindOfClass:[ABKCaptionedImageCard class]]) { return @"ABKNFCaptionedMessageCardCell"; } else if ([card isKindOfClass:[ABKClassicCard class]]) { return @"ABKNFNewsCardCell"; } else if ([card isKindOfClass:[ABKTextAnnouncementCard class]]) { return @"ABKNFCaptionedMessageCardCell"; } return nil; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { ABKCard *card = self.cards[indexPath.row]; [self handleCardClick:card]; } #pragma mark - Card Click Actions - (void)handleCardClick:(ABKCard *)card { [card logCardClicked]; NSURL *cardURL = [ABKUIURLUtils getEncodedURIFromString:card.urlString]; // URL Delegate if ([ABKUIURLUtils URLDelegate:Appboy.sharedInstance.appboyUrlDelegate handlesURL:cardURL fromChannel:ABKNewsFeedChannel withExtras:nil]) { return; } // WebView if ([ABKUIURLUtils URL:cardURL shouldOpenInWebView:card.openUrlInWebView]) { [self openURLInWebView:cardURL]; return; } // System [ABKUIURLUtils openURLWithSystem:cardURL]; } - (void)openURLInWebView:(NSURL *)url { ABKFeedWebViewController *webViewController = [[ABKFeedWebViewController alloc] init]; webViewController.url = url; webViewController.showDoneButton = self.navigationItem.rightBarButtonItem != nil; [self.navigationController pushViewController:webViewController animated:YES]; } # pragma mark - Utility Methods + (instancetype)getNavigationFeedViewController { return [[ABKNewsFeedTableViewController alloc] init]; } - (NSString *)localizedAppboyFeedString:(NSString *)key { return [ABKUIUtils getLocalizedString:key inAppboyBundle:[ABKUIUtils bundle:[ABKNewsFeedTableViewController class] channel:ABKNewsFeedChannel] table:@"AppboyFeedLocalizable"]; } # pragma mark - ABKBaseNewsFeedCellDelegate - (void)refreshTableViewCellHeights { [UIView performWithoutAnimation:^{ [self.tableView beginUpdates]; [self.tableView endUpdates]; }]; } @end ================================================ FILE: AppboyUI/ABKNewsFeed/ViewControllers/ABKNewsFeedViewController.h ================================================ #import #import "ABKNewsFeedTableViewController.h" @interface ABKNewsFeedViewController : UINavigationController /*! * This property is the table view controller which displays all the cards. It's also the root view * controller. */ @property (nonatomic) ABKNewsFeedTableViewController *newsFeed; @end ================================================ FILE: AppboyUI/ABKNewsFeed/ViewControllers/ABKNewsFeedViewController.m ================================================ #import "ABKNewsFeedViewController.h" #import "ABKNewsFeedTableViewController.h" #import "ABKUIUtils.h" @implementation ABKNewsFeedViewController - (instancetype)init { self = [super init]; if (self) { self.newsFeed = [[ABKNewsFeedTableViewController alloc] init]; [self pushViewController:self.newsFeed animated:NO]; [self addDoneButton]; #if !TARGET_OS_TV if (@available(iOS 15.0, *)) { self.view.backgroundColor = UIColor.systemGroupedBackgroundColor; } #endif } return self; } - (void)awakeFromNib { [super awakeFromNib]; self.newsFeed = self.viewControllers.firstObject; [self addDoneButton]; } - (void)addDoneButton { UIBarButtonItem *closeBarButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(dismissNewsFeed:)]; [self.newsFeed.navigationItem setRightBarButtonItem:closeBarButton]; } - (IBAction)dismissNewsFeed:(id)sender { [self dismissViewControllerAnimated:YES completion:nil]; } @end ================================================ FILE: AppboyUI/ABKNewsFeed/ViewControllers/Cells/ABKNFBannerCardCell.h ================================================ #import "ABKNFBaseCardCell.h" #import "ABKBannerCard.h" @interface ABKNFBannerCardCell : ABKNFBaseCardCell @property (nonatomic) IBOutlet UIImageView *bannerImageView; @property (nonatomic) IBOutlet NSLayoutConstraint *imageRatioConstraint; /*! * @discussion Programmatic initialization and layout of the banner imageView, exposed for customization. */ - (void)setUpBannerImageView; - (void)applyCard:(ABKCard *)bannerCard; @end ================================================ FILE: AppboyUI/ABKNewsFeed/ViewControllers/Cells/ABKNFBannerCardCell.m ================================================ #import "ABKNFBannerCardCell.h" #import "Appboy.h" #import "ABKImageDelegate.h" @implementation ABKNFBannerCardCell #pragma mark - SetUp - (void)setUpUI { [super setUpUI]; [self setUpBannerImageView]; } - (void)setUpBannerImageView { self.bannerImageView = [[[self imageViewClass] alloc] init]; self.bannerImageView.contentMode = UIViewContentModeScaleAspectFit; self.bannerImageView.translatesAutoresizingMaskIntoConstraints = NO; [self.bannerImageView setContentHuggingPriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisVertical]; [self.bannerImageView setContentHuggingPriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal]; [self.bannerImageView setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisVertical]; [self.rootView addSubview:self.bannerImageView]; [self.bannerImageView.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor].active = YES; [self.bannerImageView.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor].active = YES; [self.bannerImageView.topAnchor constraintEqualToAnchor:self.rootView.topAnchor].active = YES; [self.bannerImageView.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor].active = YES; NSLayoutConstraint *estimatedWidth = [self.bannerImageView.widthAnchor constraintEqualToAnchor:self.rootView.widthAnchor]; estimatedWidth.priority = UILayoutPriorityDefaultHigh; estimatedWidth.active = YES; self.imageRatioConstraint = [self.bannerImageView.widthAnchor constraintEqualToAnchor:self.bannerImageView.heightAnchor multiplier:355.0/79.0]; self.imageRatioConstraint.priority = UILayoutPriorityRequired-1; self.imageRatioConstraint.active = YES; NSLayoutConstraint *estimatedHeight = [self.rootView.heightAnchor constraintGreaterThanOrEqualToConstant:100]; estimatedHeight.priority = UILayoutPriorityDefaultLow; estimatedHeight.active = YES; } #pragma mark - ApplyCard - (void)applyCard:(ABKCard *)card { if (![card isKindOfClass:[ABKBannerCard class]]) { return; } [super applyCard:card]; ABKBannerCard *bannerCard = (ABKBannerCard *)card; [self updateImageRatioConstraintToRatio:bannerCard.imageAspectRatio]; [self setNeedsUpdateConstraints]; [self setNeedsLayout]; if (![Appboy sharedInstance].imageDelegate) { NSLog(@"[APPBOY][WARN] %@ %s", @"ABKImageDelegate on Appboy is nil. Image loading may be disabled.", __PRETTY_FUNCTION__); return; } typeof(self) __weak weakSelf = self; [[Appboy sharedInstance].imageDelegate setImageForView:self.bannerImageView showActivityIndicator:NO withURL:[NSURL URLWithString:bannerCard.image] imagePlaceHolder:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, NSInteger cacheType, NSURL * _Nullable imageURL) { if (weakSelf == nil) { return; } if (image) { dispatch_async(dispatch_get_main_queue(), ^{ CGFloat newRatio = image.size.width / image.size.height; if (fabs(newRatio - weakSelf.imageRatioConstraint.multiplier) > 0.1f) { [weakSelf updateImageRatioConstraintToRatio:newRatio]; [weakSelf setNeedsUpdateConstraints]; [weakSelf setNeedsLayout]; } }); } else { dispatch_async(dispatch_get_main_queue(), ^{ weakSelf.bannerImageView.image = [weakSelf getPlaceHolderImage]; }); } }]; } - (void)updateImageRatioConstraintToRatio:(CGFloat)newRatio { if (self.imageRatioConstraint) { self.imageRatioConstraint.active = NO; } self.imageRatioConstraint = [self.bannerImageView.widthAnchor constraintEqualToAnchor:self.bannerImageView.heightAnchor multiplier:newRatio]; self.imageRatioConstraint.priority = UILayoutPriorityRequired-1; NSLayoutConstraint *estimatedHeight = [self.rootView.heightAnchor constraintGreaterThanOrEqualToConstant:ceil(self.rootView.frame.size.width/self.imageRatioConstraint.multiplier)]; estimatedHeight.priority = UILayoutPriorityDefaultLow; estimatedHeight.active = YES; self.imageRatioConstraint.active = YES; } @end ================================================ FILE: AppboyUI/ABKNewsFeed/ViewControllers/Cells/ABKNFBaseCardCell.h ================================================ #import #import "ABKCard.h" @protocol ABKBaseNewsFeedCellDelegate - (void)refreshTableViewCellHeights; @end extern CGFloat ABKNFLabelHorizontalSpace; extern CGFloat ABKNFLabelVerticalSpace; extern CGFloat ABKNFTopSpace; @interface ABKNFBaseCardCell : UITableViewCell + (UIColor *)ABKNFDescriptionLabelColor; + (UIColor *)ABKNFTitleLabelColor; + (UIColor *)ABKNFTitleLabelColorOnGray; /*! * This view displays the card contents and is the base view container for each card. To change or * configure the outline of the card like card width, background color board width, etc, you can * update this property accordingly. */ @property (nonatomic) IBOutlet UIView *rootView; /*! * This is the triangle image which shows if a card has been viewed by the user. */ @property (nonatomic) IBOutlet UIImageView *unreadIndicatorView; @property (nonatomic) id delegate; /*! * Card root view related constraints */ @property (nonatomic) IBOutlet NSLayoutConstraint *rootViewLeadingConstraint; @property (nonatomic) IBOutlet NSLayoutConstraint *rootViewTrailingConstraint; @property (nonatomic) IBOutlet NSLayoutConstraint *rootViewTopConstraint; @property (nonatomic) IBOutlet NSLayoutConstraint *rootViewBottomConstraint; /*! * These are basic UI configuration for the News Feed. They are set to the default value in `setUp` * method. * * It's recommended to set the values before the view is displayed. */ @property CGFloat cardSidePadding; @property CGFloat cardSpacing; @property (nonatomic) BOOL hideUnreadIndicator; /*! * @discussion Initialization of cell called even with storyboard/XIB, exposed for customization. */ - (void)setUp; /*! * @discussion Programmatic initialization and layout cell, exposed for customization. */ - (void)setUpUI; /*! * @discussion Programmatic initialization and layout of cell rootView, exposed for customization. */ - (void)setUpRootView; /*! * @discussion Programmatic initialization and layout of cell border, exposed for customization. */ - (void)setUpRootViewBorder; /*! * @discussion Programmatic initialization and layout of unread indicator image, exposed for customization. */ - (void)setUpUnreadIndicatorView; /*! * @param card The card model for the cell. * * @discussion Apply the data from the given card to the card cell. */ - (void)applyCard:(ABKCard *)card; /*! * @discussion This is a utility method to return the place holder image. */ - (UIImage *)getPlaceHolderImage; /*! * @discussion This is a utility method to return the image view class from the ABKImageDelegate. */ - (Class)imageViewClass; @end ================================================ FILE: AppboyUI/ABKNewsFeed/ViewControllers/Cells/ABKNFBaseCardCell.m ================================================ #import "ABKNFBaseCardCell.h" #import "ABKBannerCard.h" #import "ABKTextAnnouncementCard.h" #import "ABKCaptionedImageCard.h" #import "ABKClassicCard.h" #import "ABKUIUtils.h" #import "ABKImageDelegate.h" CGFloat ABKNFLabelHorizontalSpace = 22.0; CGFloat ABKNFLabelVerticalSpace = 13.0; CGFloat ABKNFTopSpace = 7.0; static CGFloat AppboyCardSidePadding = 10.0; static CGFloat AppboyCardSpacing = 20.0; static CGFloat AppboyCardBorderWidth = 0.5; static CGFloat AppboyCardCornerRadius = 3.0; @implementation ABKNFBaseCardCell + (UIColor *)ABKNFDescriptionLabelColor { return [ABKUIUtils dynamicColorForLightColor:[UIColor colorWithRed:0.1747547901 green:0.1760663777 blue:0.1758382755 alpha:1] darkColor:[UIColor lightTextColor]]; } + (UIColor *)ABKNFTitleLabelColor { return [ABKUIUtils dynamicColorForLightColor:[UIColor colorWithRed:0.25098039220000001 green:0.27657390510000002 blue:0.32259352190000001 alpha:1] darkColor:[UIColor lightTextColor]]; } + (UIColor *)ABKNFTitleLabelColorOnGray { return [ABKUIUtils dynamicColorForLightColor:[UIColor colorWithRed:0.25327896900000002 green:0.28065123180000001 blue:0.32005588499999998 alpha:1] darkColor:[UIColor lightTextColor]]; } #pragma mark - Initialization - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { [self setUp]; [self setUpUI]; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { if (self = [super initWithCoder:aDecoder]) { [self setUp]; } return self; } #pragma mark - SetUp - (void)setUp { _cardSidePadding = AppboyCardSidePadding; _cardSpacing = AppboyCardSpacing; } - (void)setUpUI { [self setUpRootView]; [self setUpRootViewBorder]; [self setUpUnreadIndicatorView]; } - (void)setUpRootView { self.backgroundColor = [UIColor clearColor]; self.contentView.backgroundColor = [UIColor clearColor]; self.contentView.autoresizingMask = UIViewAutoresizingFlexibleHeight; self.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; self.selectionStyle = UITableViewCellSelectionStyleNone; self.rootView = [[UIView alloc] init]; self.rootView.translatesAutoresizingMaskIntoConstraints = NO; [[self contentView] addSubview:self.rootView]; if (@available(iOS 13.0, *)) { self.rootView.backgroundColor = [UIColor systemBackgroundColor]; } else { self.rootView.backgroundColor = [UIColor whiteColor]; } self.rootViewTopConstraint = [self.rootView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor constant:AppboyCardSpacing / 2.0]; self.rootViewBottomConstraint = [self.contentView.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor constant:AppboyCardSpacing / 2.0]; self.rootViewLeadingConstraint = [self.rootView.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor constant:AppboyCardSidePadding]; self.rootViewTrailingConstraint = [self.contentView.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor constant:AppboyCardSidePadding]; [NSLayoutConstraint activateConstraints:@[self.rootViewTopConstraint, self.rootViewBottomConstraint, self.rootViewLeadingConstraint, self.rootViewTrailingConstraint]]; } - (void)setUpRootViewBorder { self.rootView.layer.cornerRadius = AppboyCardCornerRadius; self.rootView.layer.masksToBounds = YES; self.rootView.layer.borderColor = [UIColor colorWithWhite:0.75f alpha:1.0].CGColor; self.rootView.layer.borderWidth = AppboyCardBorderWidth; self.rootViewTopConstraint.constant = AppboyCardSpacing / 2.0; self.rootViewBottomConstraint.constant = AppboyCardSpacing / 2.0; self.rootViewLeadingConstraint.constant = AppboyCardSidePadding; self.rootViewTrailingConstraint.constant = AppboyCardSidePadding; } - (void)setUpUnreadIndicatorView { self.unreadIndicatorView = [[UIImageView alloc] initWithImage:[ABKUIUtils imageNamed:@"Icons_Read" bundle:[ABKNFBaseCardCell class] channel:ABKNewsFeedChannel]]; self.unreadIndicatorView.translatesAutoresizingMaskIntoConstraints = NO; self.unreadIndicatorView.highlightedImage = [ABKUIUtils imageNamed:@"Icons_Unread" bundle:[ABKNFBaseCardCell class] channel:ABKNewsFeedChannel]; [self.rootView addSubview:self.unreadIndicatorView]; [self.unreadIndicatorView.heightAnchor constraintEqualToConstant:20].active = YES; [self.unreadIndicatorView.widthAnchor constraintEqualToConstant:20].active = YES; [self.unreadIndicatorView.topAnchor constraintEqualToAnchor:self.rootView.topAnchor].active = YES; [self.rootView.trailingAnchor constraintEqualToAnchor:self.unreadIndicatorView.trailingAnchor].active = YES; self.unreadIndicatorView.image = [self.unreadIndicatorView.image imageFlippedForRightToLeftLayoutDirection]; } # pragma mark - Cell UI Configuration - (void)setHideUnreadIndicator:(BOOL)hideUnreadIndicator { if(self.hideUnreadIndicator != hideUnreadIndicator) { _hideUnreadIndicator = hideUnreadIndicator; self.unreadIndicatorView.hidden = hideUnreadIndicator; } } #pragma mark - ApplyCard - (void)applyCard:(ABKCard *)card { if(!self.hideUnreadIndicator) { self.unreadIndicatorView.highlighted = !card.viewed; } } #pragma mark - Utiliy Methods - (UIImage *)getPlaceHolderImage { return [ABKUIUtils imageNamed:@"img-noimage-lrg" bundle:[ABKNFBaseCardCell class] channel:ABKNewsFeedChannel]; } - (Class)imageViewClass { if ([Appboy sharedInstance].imageDelegate) { return [[Appboy sharedInstance].imageDelegate imageViewClass]; } return [UIImageView class]; } - (void)awakeFromNib { [super awakeFromNib]; [self setUpRootViewBorder]; self.unreadIndicatorView.image = [self.unreadIndicatorView.image imageFlippedForRightToLeftLayoutDirection]; } @end ================================================ FILE: AppboyUI/ABKNewsFeed/ViewControllers/Cells/ABKNFCaptionedMessageCardCell.h ================================================ #import "ABKNFBaseCardCell.h" #import "ABKCaptionedImageCard.h" #import "ABKTextAnnouncementCard.h" @interface ABKNFCaptionedMessageCardCell : ABKNFBaseCardCell @property (class, nonatomic) UIColor *titleLabelColor; @property (class, nonatomic) UIColor *descriptionLabelColor; @property (class, nonatomic) UIColor *linkLabelColor; @property (nonatomic) IBOutlet UIImageView *captionedImageView; @property (nonatomic) IBOutlet UILabel *titleLabel; @property (nonatomic) IBOutlet UILabel *descriptionLabel; @property (nonatomic) IBOutlet UIView *titleBackgroundView; @property (nonatomic) IBOutlet UILabel *linkLabel; @property (nonatomic) IBOutlet NSLayoutConstraint *imageHeightConstraint; @property (nonatomic) IBOutlet NSLayoutConstraint *bodyAndLinkConstraint; /*! * @discussion Programmatic initialization and layout of the title background view, grey bar that the title label is in. * Exposed for customization. */ - (void)setUpTitleBackgroundView; /*! * @discussion Programmatic initialization and layout of the title label. Exposed for customization. */ - (void)setUpTitleLabel; /*! * @discussion Programmatic initialization and layout of the description label. Exposed for customization. */ - (void)setUpDescriptionLabel; /*! * @discussion Programmatic initialization and layout of the link label. Exposed for customization. */ - (void)setUpLinkLabel; /*! * @discussion Programmatic initialization and layout of image view. Exposed for customization. */ - (void)setUpCaptionedImageView; /*! * @discussion Configures fonts of labels with dynamic type on supported versions of iOS uses older font style * on earlier versions. Exposed for customization. */ - (void)setUpFonts; /*! * This method adjusts the bodyAndLinkConstraint and hides or shows the link label. */ - (void)hideLinkLabel:(BOOL)hide; - (void)applyCard:(ABKCaptionedImageCard *)captionedImageCard; @end ================================================ FILE: AppboyUI/ABKNewsFeed/ViewControllers/Cells/ABKNFCaptionedMessageCardCell.m ================================================ #import "ABKNFCaptionedMessageCardCell.h" #import "Appboy.h" #import "ABKImageDelegate.h" #import "ABKUIUtils.h" @implementation ABKNFCaptionedMessageCardCell static UIColor *_titleLabelColor = nil; static UIColor *_descriptionLabelColor = nil; static UIColor *_linkLabelColor = nil; + (UIColor *)titleLabelColor { if (_titleLabelColor == nil) { _titleLabelColor = [ABKNFBaseCardCell ABKNFTitleLabelColor]; } return _titleLabelColor; } + (void)setTitleLabelColor:(UIColor *)titleLabelColor { _titleLabelColor = titleLabelColor; } + (UIColor *)descriptionLabelColor { if (_descriptionLabelColor == nil) { _descriptionLabelColor = [ABKNFBaseCardCell ABKNFDescriptionLabelColor]; } return _descriptionLabelColor; } + (void)setDescriptionLabelColor:(UIColor *)descriptionLabelColor { _descriptionLabelColor = descriptionLabelColor; } + (UIColor *)linkLabelColor { if (_linkLabelColor == nil) { _linkLabelColor = [ABKUIUtils dynamicColorForLightColor:[UIColor blackColor] darkColor:[UIColor whiteColor]]; } return _linkLabelColor; } + (void)setLinkLabelColor:(UIColor *)linkLabelColor{ _linkLabelColor = linkLabelColor; } #pragma mark - SetUp - (void)setUpUI { [super setUpUI]; [self setUpTitleBackgroundView]; [self setUpTitleLabel]; [self setUpDescriptionLabel]; [self setUpLinkLabel]; [self setUpCaptionedImageView]; [self setUpFonts]; } - (void)setUpTitleBackgroundView { self.titleBackgroundView = [[UIView alloc] init]; self.titleBackgroundView.translatesAutoresizingMaskIntoConstraints = NO; if (@available(iOS 13.0, *)) { self.titleBackgroundView.backgroundColor = [UIColor systemGroupedBackgroundColor]; } else { self.titleBackgroundView.backgroundColor = [UIColor groupTableViewBackgroundColor]; } [self.rootView addSubview:self.titleBackgroundView]; [self.titleBackgroundView.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor].active = YES; [self.titleBackgroundView.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor].active = YES; [self.unreadIndicatorView removeFromSuperview]; [self.titleBackgroundView addSubview:self.unreadIndicatorView]; [self.unreadIndicatorView.topAnchor constraintEqualToAnchor:self.titleBackgroundView.topAnchor].active = YES; [self.unreadIndicatorView.trailingAnchor constraintEqualToAnchor:self.titleBackgroundView.trailingAnchor].active = YES; } - (void)setUpTitleLabel { self.titleLabel = [[UILabel alloc] init]; self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO; self.titleLabel.textColor = [self class].titleLabelColor; self.titleLabel.text = @"Title"; self.titleLabel.numberOfLines = 2; self.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail; [self.titleBackgroundView addSubview:self.titleLabel]; [self.titleLabel setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical]; [self.titleLabel setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical]; [self.titleLabel.leadingAnchor constraintEqualToAnchor:self.titleBackgroundView.leadingAnchor constant:ABKNFLabelHorizontalSpace].active = YES; [self.titleBackgroundView.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor constant:ABKNFLabelHorizontalSpace].active = YES; [self.titleLabel.topAnchor constraintEqualToAnchor:self.titleBackgroundView.topAnchor constant:10].active = YES; [self.titleBackgroundView.bottomAnchor constraintEqualToAnchor:self.titleLabel.bottomAnchor constant:10].active = YES; } - (void)setUpDescriptionLabel { self.descriptionLabel = [[UILabel alloc] init]; self.descriptionLabel.textColor = [self class].descriptionLabelColor; self.descriptionLabel.text = @"Description"; self.descriptionLabel.translatesAutoresizingMaskIntoConstraints = NO; self.descriptionLabel.numberOfLines = 0; self.descriptionLabel.lineBreakMode = NSLineBreakByWordWrapping; [self.descriptionLabel setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical]; [self.rootView addSubview:self.descriptionLabel]; [self.descriptionLabel.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor constant:ABKNFLabelHorizontalSpace].active = YES; [self.rootView.trailingAnchor constraintEqualToAnchor:self.descriptionLabel.trailingAnchor constant:ABKNFLabelHorizontalSpace].active = YES; [self.descriptionLabel.topAnchor constraintEqualToAnchor:self.titleBackgroundView.bottomAnchor constant:ABKNFLabelVerticalSpace].active = YES; [self.rootView.bottomAnchor constraintGreaterThanOrEqualToAnchor:self.descriptionLabel.bottomAnchor constant:ABKNFLabelVerticalSpace].active = YES; } - (void)setUpLinkLabel { self.linkLabel = [[UILabel alloc] init]; self.linkLabel.textColor = [self class].linkLabelColor; self.linkLabel.text = @"Link"; self.linkLabel.translatesAutoresizingMaskIntoConstraints = NO; self.linkLabel.numberOfLines = 0; self.linkLabel.lineBreakMode = NSLineBreakByCharWrapping; [self.rootView addSubview:self.linkLabel]; [self.linkLabel.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor constant:ABKNFLabelHorizontalSpace].active = YES; [self.rootView.trailingAnchor constraintEqualToAnchor:self.linkLabel.trailingAnchor constant:ABKNFLabelHorizontalSpace].active = YES; [self.linkLabel.topAnchor constraintEqualToAnchor:self.descriptionLabel.bottomAnchor constant:ABKNFLabelVerticalSpace].active = YES; self.bodyAndLinkConstraint = [self.rootView.bottomAnchor constraintEqualToAnchor:self.linkLabel.bottomAnchor constant:ABKNFLabelVerticalSpace]; self.bodyAndLinkConstraint.active = YES; } - (void)setUpCaptionedImageView { self.captionedImageView = [[[self imageViewClass] alloc] init]; self.captionedImageView.contentMode = UIViewContentModeScaleAspectFit; self.captionedImageView.translatesAutoresizingMaskIntoConstraints = NO; [self.rootView addSubview:self.captionedImageView]; [self.captionedImageView setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical]; [self.captionedImageView setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical]; [self.captionedImageView.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor].active = YES; [self.captionedImageView.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor].active = YES; [self.captionedImageView.topAnchor constraintEqualToAnchor:self.rootView.topAnchor].active = YES; NSLayoutConstraint *bottom = [self.captionedImageView.bottomAnchor constraintEqualToAnchor:self.titleBackgroundView.topAnchor]; bottom.priority = UILayoutPriorityDefaultHigh; bottom.active = YES; self.imageHeightConstraint = [self.captionedImageView.heightAnchor constraintEqualToConstant:223]; self.imageHeightConstraint.active = YES; } - (void)setUpFonts { // DynamicType self.titleLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleTitle3 weight:UIFontWeightBold]; [ABKUIUtils enableAdjustsFontForContentSizeCategory:self.titleLabel]; self.descriptionLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]; [ABKUIUtils enableAdjustsFontForContentSizeCategory:self.descriptionLabel]; self.linkLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleSubheadline weight:UIFontWeightBold]; [ABKUIUtils enableAdjustsFontForContentSizeCategory:self.linkLabel]; // Bug: On Mac Catalyst 13, allowsDefaultTighteningForTruncation defaults to YES // - Occurs only if numberOfLine is not 0 // - Default value should be NO (see documentation – https://apple.co/3bZFc8q) // - Might be fixed in a later version self.titleLabel.allowsDefaultTighteningForTruncation = NO; } - (void)hideLinkLabel:(BOOL)hide { self.linkLabel.hidden = hide; self.bodyAndLinkConstraint.constant = hide ? 0 : ABKNFLabelVerticalSpace; } #pragma mark - ApplyCard - (void)applyCard:(ABKCard *)card { [super applyCard:card]; if ([card isKindOfClass:[ABKCaptionedImageCard class]]) { [self applyCaptionedImageCard:(ABKCaptionedImageCard *)card]; } else if ([card isKindOfClass:[ABKTextAnnouncementCard class]]) { [self applyTextAnnouncementCard:(ABKTextAnnouncementCard *)card]; } } - (void)applyCaptionedImageCard:(ABKCaptionedImageCard *)captionedImageCard { self.titleLabel.text = captionedImageCard.title; self.descriptionLabel.text = captionedImageCard.cardDescription; self.linkLabel.text = captionedImageCard.domain; BOOL shouldHideLink = captionedImageCard.domain == nil || captionedImageCard.domain.length == 0; [self hideLinkLabel:shouldHideLink]; CGFloat currImageHeightConstraint = self.captionedImageView.frame.size.width / captionedImageCard.imageAspectRatio; self.imageHeightConstraint.constant = currImageHeightConstraint; [self setNeedsUpdateConstraints]; [self setNeedsDisplay]; if (![Appboy sharedInstance].imageDelegate) { NSLog(@"[APPBOY][WARN] %@ %s", @"ABKImageDelegate on Appboy is nil. Image loading may be disabled.", __PRETTY_FUNCTION__); return; } typeof(self) __weak weakSelf = self; [[Appboy sharedInstance].imageDelegate setImageForView:self.captionedImageView showActivityIndicator:NO withURL:[NSURL URLWithString:captionedImageCard.image] imagePlaceHolder:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, NSInteger cacheType, NSURL * _Nullable imageURL) { if (weakSelf == nil) { return; } if (image) { dispatch_async(dispatch_get_main_queue(), ^{ CGFloat newImageHeightConstraint = weakSelf.captionedImageView.frame.size.width * image.size.height / image.size.width; if (fabs(newImageHeightConstraint - currImageHeightConstraint) > 5e-1) { weakSelf.imageHeightConstraint.constant = newImageHeightConstraint; [weakSelf setNeedsUpdateConstraints]; [weakSelf setNeedsDisplay]; // Force a redraw, as SDWebImage 5+ consistently gets the original constraint wrong. [weakSelf.delegate refreshTableViewCellHeights]; } }); } else { dispatch_async(dispatch_get_main_queue(), ^{ weakSelf.captionedImageView.image = [weakSelf getPlaceHolderImage]; }); } }]; } - (void)applyTextAnnouncementCard:(ABKTextAnnouncementCard *)textAnnouncementCard { self.titleLabel.text = textAnnouncementCard.title; self.descriptionLabel.text = textAnnouncementCard.cardDescription; self.linkLabel.text = textAnnouncementCard.domain; BOOL shouldHideLink = textAnnouncementCard.domain == nil || textAnnouncementCard.domain.length == 0; [self hideLinkLabel:shouldHideLink]; self.imageHeightConstraint.constant = 0; [self setNeedsLayout]; } - (void)awakeFromNib { [super awakeFromNib]; [self setUpFonts]; } @end ================================================ FILE: AppboyUI/ABKNewsFeed/ViewControllers/Cells/ABKNFClassicCardCell.h ================================================ #import "ABKNFBaseCardCell.h" #import "ABKClassicCard.h" @interface ABKNFClassicCardCell : ABKNFBaseCardCell @property (class, nonatomic) UIColor *titleLabelColor; @property (class, nonatomic) UIColor *descriptionLabelColor; @property (class, nonatomic) UIColor *linkLabelColor; @property (nonatomic) IBOutlet UIImageView *classicImageView; @property (nonatomic) IBOutlet UILabel *titleLabel; @property (nonatomic) IBOutlet UILabel *descriptionLabel; @property (nonatomic) IBOutlet UILabel *linkLabel; /*! * @discussion Programmatic initialization and layout of image view. Exposed for customization. */ - (void)setUpClassicImageView; /*! * @discussion Programmatic initialization and layout of the title label. Exposed for customization. */ - (void)setUpTitleLabel; /*! * @discussion Programmatic initialization and layout of the description label. Exposed for customization. */ - (void)setUpDescriptionLabel; /*! * @discussion Programmatic initialization and layout of the link label. Exposed for customization. */ - (void)setUpLinkLabel; /*! * @discussion Configures fonts of labels with dynamic type on supported versions of iOS uses older font style * on earlier versions. Exposed for customization. */ - (void)setUpFonts; - (void)applyCard:(ABKClassicCard *)classicCard; @end ================================================ FILE: AppboyUI/ABKNewsFeed/ViewControllers/Cells/ABKNFClassicCardCell.m ================================================ #import "ABKNFClassicCardCell.h" #import "Appboy.h" #import "ABKImageDelegate.h" #import "ABKUIUtils.h" @implementation ABKNFClassicCardCell static UIColor *_titleLabelColor = nil; static UIColor *_descriptionLabelColor = nil; static UIColor *_linkLabelColor = nil; + (UIColor *)titleLabelColor { if (_titleLabelColor == nil) { _titleLabelColor = [ABKNFBaseCardCell ABKNFTitleLabelColor]; } return _titleLabelColor; } + (void)setTitleLabelColor:(UIColor *)titleLabelColor { _titleLabelColor = titleLabelColor; } + (UIColor *)descriptionLabelColor { if (_descriptionLabelColor == nil) { _descriptionLabelColor = [ABKNFBaseCardCell ABKNFDescriptionLabelColor]; } return _descriptionLabelColor; } + (void)setDescriptionLabelColor:(UIColor *)descriptionLabelColor { _descriptionLabelColor = descriptionLabelColor; } + (UIColor *)linkLabelColor { if (_linkLabelColor == nil) { _linkLabelColor = [ABKNFBaseCardCell ABKNFDescriptionLabelColor]; } return _linkLabelColor; } + (void)setLinkLabelColor:(UIColor *)linkLabelColor{ _linkLabelColor = linkLabelColor; } #pragma mark - SetUp - (void)setUpUI { [super setUpUI]; [self setUpClassicImageView]; [self setUpTitleLabel]; [self setUpDescriptionLabel]; [self setUpLinkLabel]; [self setUpFonts]; } - (void)setUpClassicImageView { self.classicImageView = [[[self imageViewClass] alloc] init]; self.classicImageView.translatesAutoresizingMaskIntoConstraints = NO; [self.rootView addSubview:self.classicImageView]; [self.classicImageView.heightAnchor constraintEqualToAnchor:self.classicImageView.widthAnchor multiplier:1.0].active = YES; [self.classicImageView.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor constant:ABKNFLabelHorizontalSpace].active = YES; [self.classicImageView.topAnchor constraintEqualToAnchor:self.rootView.topAnchor constant:ABKNFLabelVerticalSpace].active = YES; [self.rootView.bottomAnchor constraintGreaterThanOrEqualToAnchor:self.classicImageView.bottomAnchor constant:ABKNFLabelVerticalSpace].active = YES; [self.classicImageView.widthAnchor constraintEqualToAnchor:self.rootView.widthAnchor multiplier:0.177].active = YES; } - (void)setUpTitleLabel { self.titleLabel = [[UILabel alloc] init]; self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO; self.titleLabel.numberOfLines = 0; self.titleLabel.lineBreakMode = NSLineBreakByWordWrapping; self.titleLabel.textColor = [self class].titleLabelColor; self.titleLabel.text = @"Title"; [self.rootView addSubview:self.titleLabel]; [self.titleLabel.leadingAnchor constraintEqualToAnchor:self.classicImageView.trailingAnchor constant:ABKNFLabelHorizontalSpace].active = YES; [self.rootView.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor constant:ABKNFLabelHorizontalSpace].active = YES; [self.titleLabel.topAnchor constraintEqualToAnchor:self.rootView.topAnchor constant:ABKNFTopSpace].active = YES; } - (void)setUpDescriptionLabel { self.descriptionLabel = [[UILabel alloc] init]; self.descriptionLabel.translatesAutoresizingMaskIntoConstraints = NO; self.descriptionLabel.numberOfLines = 0; self.descriptionLabel.lineBreakMode = NSLineBreakByWordWrapping; self.descriptionLabel.textColor = [self class].descriptionLabelColor; self.descriptionLabel.text = @"Description"; [self.rootView addSubview:self.descriptionLabel]; [self.titleLabel.bottomAnchor constraintEqualToAnchor:self.descriptionLabel.topAnchor].active = YES; [self.descriptionLabel.leadingAnchor constraintEqualToAnchor:self.titleLabel.leadingAnchor].active = YES; [self.descriptionLabel.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor].active = YES; } - (void)setUpLinkLabel { self.linkLabel = [[UILabel alloc] init]; self.linkLabel.translatesAutoresizingMaskIntoConstraints = NO; self.linkLabel.numberOfLines = 0; self.linkLabel.lineBreakMode = NSLineBreakByCharWrapping; self.linkLabel.textColor = [self class].linkLabelColor; self.linkLabel.text = @"Link"; [self.rootView addSubview:self.linkLabel]; [self.linkLabel.leadingAnchor constraintEqualToAnchor:self.titleLabel.leadingAnchor].active = YES; [self.linkLabel.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor].active = YES; [self.linkLabel.topAnchor constraintGreaterThanOrEqualToAnchor:self.descriptionLabel.bottomAnchor constant:5].active = YES; [self.rootView.bottomAnchor constraintEqualToAnchor:self.linkLabel.bottomAnchor constant:ABKNFTopSpace].active = YES; } - (void)setUpFonts { // DynamicType self.titleLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleTitle3 weight:UIFontWeightBold]; [ABKUIUtils enableAdjustsFontForContentSizeCategory:self.titleLabel]; self.descriptionLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]; [ABKUIUtils enableAdjustsFontForContentSizeCategory:self.descriptionLabel]; self.linkLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleSubheadline weight:UIFontWeightBold]; [ABKUIUtils enableAdjustsFontForContentSizeCategory:self.linkLabel]; // Bug: On Mac Catalyst 13, allowsDefaultTighteningForTruncation defaults to YES // - Occurs only if numberOfLine is not 0 // - Default value should be NO (see documentation – https://apple.co/3bZFc8q) // - Might be fixed in a later version self.titleLabel.allowsDefaultTighteningForTruncation = NO; } #pragma mark - ApplyCard - (void)applyCard:(ABKCard *)card { [super applyCard:card]; if (![card isKindOfClass:[ABKClassicCard class]]) { return; } ABKClassicCard *classicCard = (ABKClassicCard *)card; self.titleLabel.text = classicCard.title; self.descriptionLabel.text = classicCard.cardDescription; self.linkLabel.text = classicCard.domain; if (![Appboy sharedInstance].imageDelegate) { NSLog(@"[APPBOY][WARN] %@ %s", @"ABKImageDelegate on Appboy is nil. Image loading may be disabled.", __PRETTY_FUNCTION__); return; } [[Appboy sharedInstance].imageDelegate setImageForView:self.classicImageView showActivityIndicator:NO withURL:[NSURL URLWithString:classicCard.image] imagePlaceHolder:[self getPlaceHolderImage] completed:nil]; } - (void)awakeFromNib { [super awakeFromNib]; [self setUpFonts]; } @end ================================================ FILE: AppboyUI/ABKUIUtils/ABKSDWebImageImageDelegate.h ================================================ #import "ABKImageDelegate.h" NS_ASSUME_NONNULL_BEGIN @interface ABKSDWebImageImageDelegate : NSObject @end NS_ASSUME_NONNULL_END ================================================ FILE: AppboyUI/ABKUIUtils/ABKSDWebImageImageDelegate.m ================================================ #import "ABKSDWebImageImageDelegate.h" #import "ABKSDWebImageProxy.h" #import @implementation ABKSDWebImageImageDelegate - (void)setImageForView:(UIImageView *)imageView showActivityIndicator:(BOOL)showActivityIndicator withURL:(nullable NSURL *)imageURL imagePlaceHolder:(nullable UIImage *)placeHolder completed:(nullable void (^)(UIImage * _Nullable image, NSError * _Nullable error, NSInteger cacheType, NSURL * _Nullable imageURL))completion { [ABKSDWebImageProxy setImageForView:imageView showActivityIndicator:showActivityIndicator withURL:imageURL imagePlaceHolder:placeHolder completed:completion]; } - (void)loadImageWithURL:(nullable NSURL *)url options:(ABKImageOptions)options completed:(nullable void(^)(UIImage *image, NSData *data, NSError *error, NSInteger cacheType, BOOL finished, NSURL *imageURL))completion { [ABKSDWebImageProxy loadImageWithURL:url options:options completed:completion]; } - (void)diskImageExistsForURL:(nullable NSURL *)url completed:(nullable void (^)(BOOL isInCache))completion { [ABKSDWebImageProxy diskImageExistsForURL:url completed:completion]; } - (nullable UIImage *)imageFromCacheForURL:(nullable NSURL *)url { return [ABKSDWebImageProxy imageFromCacheForKey:[ABKSDWebImageProxy cacheKeyForURL:url]]; } - (Class)imageViewClass { return [SDAnimatedImageView class]; } @end ================================================ FILE: AppboyUI/ABKUIUtils/ABKUIURLUtils.h ================================================ #import #import #import "ABKURLDelegate.h" @interface ABKUIURLUtils : NSObject + (BOOL)URLDelegate:(id)urlDelegate handlesURL:(NSURL *)url fromChannel:(ABKChannel)channel withExtras:(NSDictionary *)extras; + (BOOL)URL:(NSURL *)url shouldOpenInWebView:(BOOL)openUrlInWebView; + (BOOL)URLHasSystemScheme:(NSURL *)url; + (void)openURLWithSystem:(NSURL *)url; + (UIViewController *)topmostViewControllerWithRootViewController:(UIViewController *)viewController; + (void)displayModalWebViewWithURL:(NSURL *)url topmostViewController:(UIViewController *)topmostViewController; + (NSURL *)getEncodedURIFromString:(NSString *)uriString; @end ================================================ FILE: AppboyUI/ABKUIUtils/ABKUIURLUtils.m ================================================ #import "ABKUIURLUtils.h" #import "ABKUIUtils.h" #import "ABKModalWebViewController.h" #import "Appboy.h" @interface ABKUIURLUtils () + (NSString *)trim:(NSString *)string; @end @implementation ABKUIURLUtils + (BOOL)URLDelegate:(id)urlDelegate handlesURL:(NSURL *)url fromChannel:(ABKChannel)channel withExtras:(NSDictionary *)extras { if (![ABKUIURLUtils URLDelegateIsValid:urlDelegate]) { NSLog(@"Not handling URL %@ with invalid ABKURLDelegate %@.", url.absoluteString, urlDelegate); return NO; } if ([urlDelegate handleAppboyURL:url fromChannel:channel withExtras:extras]) { NSLog(@"Handled URL %@ with external ABKURLDelegate %@.", url.absoluteString, urlDelegate); return YES; } return NO; } + (BOOL)URLDelegateIsValid:(id)urlDelegate { return [urlDelegate respondsToSelector:@selector(handleAppboyURL:fromChannel:withExtras:)]; } + (BOOL)URL:(NSURL *)url shouldOpenInWebView:(BOOL)openUrlInWebView { if ([ABKUIUtils objectIsValidAndNotEmpty:url.absoluteString] && openUrlInWebView) { if ([ABKUIURLUtils URLHasValidWebScheme:url]) { return YES; } else { NSLog(@"Unsupported web URL scheme received: %@. Not opening URL in web view.", url.absoluteString); } } return NO; } + (BOOL)URLHasValidWebScheme:(NSURL *)url { return ([ABKUIUtils string:[url.scheme lowercaseString] isEqualToString:@"http"] || [ABKUIUtils string:[url.scheme lowercaseString] isEqualToString:@"https"]); } + (BOOL)URLHasSystemScheme:(NSURL *)url { static dispatch_once_t once; static NSSet *systemSchemes; dispatch_once(&once, ^{ systemSchemes = [NSSet setWithArray:@[ @"mailto", @"tel", @"facetime", @"facetime-audio", @"sms" ]]; }); return [systemSchemes containsObject:[url.scheme lowercaseString]]; } + (void)openURLWithSystem:(NSURL *)url { if (![NSThread isMainThread]) { dispatch_sync(dispatch_get_main_queue(), ^{ [self openURL:url]; }); } else { [self openURL:url]; } } + (void)openURL:(NSURL *)url { if (@available(iOS 13.0, *)) { UIWindowScene *windowScene = ABKUIUtils.activeWindowScene; if (windowScene) { [windowScene openURL:url options:nil completionHandler:nil]; return; } } if (@available(iOS 10.0, *)) { [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; return; } [[UIApplication sharedApplication] openURL:url]; } + (UIViewController *)topmostViewControllerWithRootViewController:(UIViewController *)viewController { while (viewController.presentedViewController) { viewController = viewController.presentedViewController; } return viewController; } + (void)displayModalWebViewWithURL:(NSURL *)URL topmostViewController:(UIViewController *)topmostViewController { ABKModalWebViewController *webViewController = [[ABKModalWebViewController alloc] init]; webViewController.url = URL; [topmostViewController presentViewController:webViewController animated:YES completion:nil]; } + (NSURL *)getEncodedURIFromString:(NSString *)uriString { if (![ABKUIUtils objectIsValidAndNotEmpty:uriString]) { return nil; } uriString = [ABKUIURLUtils trim:uriString]; NSURL *parsedUrl = [NSURL URLWithString:uriString]; // If the uriString is an invalid uri, e.g. an uri with unicode, URLWithString: will return nil. if (!parsedUrl) { // When the uriString has unicode, we have to escape those characters uriString = [uriString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; parsedUrl = [NSURL URLWithString:uriString]; } return parsedUrl; } + (NSString *)trim:(NSString *)string { if ([string isKindOfClass:[NSString class]]) { return [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; } NSLog(@"Calling `trim` with invalid class: %@, value: %@. Returning nil.", [string class], string); return nil; } @end ================================================ FILE: AppboyUI/ABKUIUtils/ABKUIUtils.h ================================================ #import #import #import "Appboy.h" #define ABK_CGFLT_EQ(lhs, rhs) (fabs(lhs - rhs) < 10 * FLT_EPSILON * fabs(lhs + rhs)) @interface ABKUIUtils : NSObject /*! * The currently active UIWindowScene. */ @property (class, nonatomic, readonly) UIWindowScene *activeWindowScene API_AVAILABLE(ios(13.0)); /*! * The currently active application UIWindow. */ @property (class, nonatomic, readonly) UIWindow *activeApplicationWindow; /*! * The currently active application UIViewController. */ @property (class, nonatomic, readonly) UIViewController *activeApplicationViewController; /*! * The current application status bar hidden state. */ @property (class, readonly) BOOL applicationStatusBarHidden; /*! * The current application status bar style. */ @property (class, readonly) UIStatusBarStyle applicationStatusBarStyle; /*! * Given a class and a channel, this method searches across multiple locations and returns the appropriate * bundle. * @param bundleClass The class associated with the bundle. * @param channel The channel associated with the bundle. * @returns The bundle if available, nil otherwise. */ + (NSBundle *)bundle:(Class)bundleClass channel:(ABKChannel)channel; + (UIImage *)imageNamed:(NSString *)name bundle:(Class)bundleClass channel:(ABKChannel)channel; + (NSString *)getLocalizedString:(NSString *)key inAppboyBundle:(NSBundle *)appboyBundle table:(NSString *)table; + (BOOL)objectIsValidAndNotEmpty:(id)object; + (Class)getModalFeedViewControllerClass; + (BOOL)isNotchedPhone; + (UIInterfaceOrientation)getInterfaceOrientation; + (CGSize)getStatusBarSize; + (UIColor *)dynamicColorForLightColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor; + (BOOL)string:(NSString *)string1 isEqualToString:(NSString *)string2; /*! * Verifies that one of the responders in the responder chain is kind of class aClass. * @param responder The start of the UIResponder chain. * @param aClass The UIResponder subclass looked for in the responder chain. * @return YES if aClass is found in the responder chain, NO otherwise. */ + (BOOL)responderChainOf:(UIResponder *)responder hasKindOfClass:(Class)aClass; /*! * Verifies that one of the responders in the responder chain is prefixed by prefix. * @param responder The start of the UIResponder chain. * @param prefix The prefix looked for in the responder chain. * @return YES if a class prefixed by prefix is found in the responder chain, NO otherwise. */ + (BOOL)responderChainOf:(UIResponder *)responder hasClassPrefixedWith:(NSString *)prefix; /*! * Creates an instance of the font associated with the text style and scaled appropriately for the * user's selected content size category. * * @warning On iOS 10 / tvOS 10 and below, this method does not apply the text style to the * resulting font. The font size is chosen according to https://apple.co/3snncd9 (Large / Default). * * @param textStyle The text style to use * @param weight The weight of the font * @return The font corresponding to the text style with weight applied to it. */ + (UIFont *)preferredFontForTextStyle:(UIFontTextStyle)textStyle weight:(UIFontWeight)weight; /*! * Enables `adjustsFontForContentSizeCategory` on the label if available (iOS 10+). * * This method has no effect on iOS / tvOS versions prior to 10.0. * * @param label Any object conforming to `UIContentSizeCategoryAdjusting` */ + (void)enableAdjustsFontForContentSizeCategory:(id)label; @end ================================================ FILE: AppboyUI/ABKUIUtils/ABKUIUtils.m ================================================ #import "ABKUIUtils.h" #import "ABKSDWebImageProxy.h" static NSString *const LocalizedAppboyStringNotFound = @"not found"; static NSUInteger const iPhoneXHeight = 2436.0; // iPhone 12 mini simulator is also this size static NSUInteger const iPhoneXRHeight = 1792.0; static NSUInteger const iPhoneXSMaxHeight = 2688.0; static NSUInteger const iPhoneXRScaledHeight = 1624.0; static NSUInteger const iPhone12 = 2532.0; // iPhone 12 pro is also this size static NSUInteger const iPhone12ProMax = 2778.0; static NSUInteger const iPhone12Mini = 2340.0; // Bundles static NSString * const ABKUIPodCCBundleName = @"AppboyUI.ContentCards.bundle"; static NSString * const ABKUIPodIAMBundleName = @"AppboyUI.InAppMessage.bundle"; static NSString * const ABKUIPodNFBundleName = @"AppboyUI.NewsFeed.bundle"; @implementation ABKUIUtils #pragma mark - Bundle Helper + (NSBundle *)bundle:(Class)bundleClass channel:(ABKChannel)channel { NSBundle *bundle; // SPM #if SWIFT_PACKAGE bundle = SWIFTPM_MODULE_BUNDLE; if (bundle != nil) { return bundle; } #endif // Cocoapods switch (channel) { case ABKContentCardChannel: bundle = [self bundleForName:ABKUIPodCCBundleName class:bundleClass]; break; case ABKInAppMessageChannel: bundle = [self bundleForName:ABKUIPodIAMBundleName class:bundleClass]; break; case ABKNewsFeedChannel: bundle = [self bundleForName:ABKUIPodNFBundleName class:bundleClass]; break; default: NSLog(@"Warning: Received bundle request for unsupported channel: %ld", (long)channel); break; } if (bundle != nil) { return bundle; } return [NSBundle bundleForClass:bundleClass]; } + (nullable NSBundle *)bundleForName:(NSString *)name class:(Class)bundleClass { NSURL *bundleURL = [[NSBundle bundleForClass:bundleClass].resourceURL URLByAppendingPathComponent:name]; if ([bundleURL checkResourceIsReachableAndReturnError:nil]) { return [NSBundle bundleWithURL:bundleURL]; } return nil; } #pragma mark - View Hierarchy Helpers // Used in unit tests to mock the UIApplication instance used. + (UIApplication *)application { return UIApplication.sharedApplication; } + (UIWindowScene *)activeWindowScene { UIWindowScene *windowScene; UIWindowScene *activeWindowScene; // Loop over the connected window scenes to find the last foreground active // one. If no scene is currently in foreground state, fallback to last window // scene in hierarchy. for (UIScene *scene in [self application].connectedScenes) { if (![scene isKindOfClass:[UIWindowScene class]]) { continue; } windowScene = (UIWindowScene *)scene; if (scene.activationState == UISceneActivationStateForegroundActive) { activeWindowScene = windowScene; } } return activeWindowScene ?: windowScene; } + (UIWindow *)activeApplicationWindow { if (@available(iOS 13.0, tvOS 13.0, *)) { UIWindow *window = [self selectApplicationWindow:ABKUIUtils.activeWindowScene.windows]; if (window) { return window; } } return [self selectApplicationWindow:[self application].windows]; } + (UIViewController *)activeApplicationViewController { return ABKUIUtils.activeApplicationWindow.rootViewController; } + (BOOL)applicationStatusBarHidden { UIViewController *viewController = self.activeApplicationViewController; while (viewController.childViewControllerForStatusBarHidden) { viewController = viewController.childViewControllerForStatusBarHidden; } return viewController.prefersStatusBarHidden; } + (UIStatusBarStyle)applicationStatusBarStyle { UIViewController *viewController = self.activeApplicationViewController; while (viewController.childViewControllerForStatusBarStyle) { viewController = viewController.childViewControllerForStatusBarStyle; } return viewController.preferredStatusBarStyle; } /*! * Selects the window most likely to be the application window among an array of windows. * * @discussion The application window should most likely be the bottommost window with a windowLevel * set to UIWindowLevelNormal (excluding a potential ABKInAppMessageWindow currently * being displayed). If no window respecting that condition is found, fallback to the first * window in the hierarchy. * * @param windows An array of UIWindow * @returns The UIWindow most likely to be the application window, nil if windows param is empty */ + (UIWindow *)selectApplicationWindow:(NSArray *)windows { // Dynamically gets ABKInAppMessageWindow class as it is part of AppboyUI Class ABKInAppMessageWindow = NSClassFromString(@"ABKInAppMessageWindow"); // Holds all windows excluding any `ABKInAppMessageWindow` NSMutableArray *filteredWindows = [NSMutableArray array]; for (UIWindow *window in windows) { // Ignores ABKInAppMessageWindow if (ABKInAppMessageWindow && [window isKindOfClass:[ABKInAppMessageWindow class]]) { continue; } // Assumes that the application window has a windowLevel set to // UIWindowLevelNormal if (window.windowLevel == UIWindowLevelNormal) { return window; } [filteredWindows addObject:window]; } // Fallback to first window in hierarchy return filteredWindows.firstObject; } #pragma mark - Methods + (NSString *)getLocalizedString:(NSString *)key inAppboyBundle:(NSBundle *)appboyBundle table:(NSString *)table { // Check if the app has a customized localization for the given key NSString *localizedString = [[NSBundle mainBundle] localizedStringForKey:key value:LocalizedAppboyStringNotFound table:nil]; if ([ABKUIUtils string:localizedString isEqualToString:LocalizedAppboyStringNotFound]) { // Check Braze's localization in the given bundle for (NSString *language in [[NSBundle mainBundle] preferredLocalizations]) { if ([[appboyBundle localizations] containsObject:language]) { NSBundle *languageBundle = [NSBundle bundleWithPath:[appboyBundle pathForResource:language ofType:@"lproj"]]; localizedString = [languageBundle localizedStringForKey:key value:LocalizedAppboyStringNotFound table:table]; break; } } if ([ABKUIUtils string:localizedString isEqualToString:LocalizedAppboyStringNotFound]) { // Couldn't find Braze's localization for the given key, fetch the default value for the key // from Base.lproj. NSBundle *appboyBaseBundle = [NSBundle bundleWithPath:[appboyBundle pathForResource:@"Base" ofType:@"lproj"]]; localizedString = [appboyBaseBundle localizedStringForKey:key value:LocalizedAppboyStringNotFound table:table]; } } return localizedString; } + (BOOL)objectIsValidAndNotEmpty:(id)object { if (object != nil && object != [NSNull null]) { if ([object isKindOfClass:[NSArray class]] || [object isKindOfClass:[NSDictionary class]] || [object isKindOfClass:[NSString class]]) { return ![ABKUIUtils isEmpty:object]; } if ([object isKindOfClass:[NSURL class]]) { NSString *string = [(NSURL *)object absoluteString]; return [string length] != 0; } return YES; } return NO; } // Calls AppboyKit private abk_isEmpty method on object + (BOOL)isEmpty:(id)object { SEL sel = NSSelectorFromString(@"abk_isEmpty"); IMP imp = [object methodForSelector:sel]; return ((BOOL (*)(id, SEL))imp)(object, sel); } + (Class)getModalFeedViewControllerClass { return NSClassFromString(@"ABKNewsFeedViewController"); } + (BOOL)isNotchedPhone { return ([[UIScreen mainScreen] nativeBounds].size.height == iPhoneXHeight || [[UIScreen mainScreen] nativeBounds].size.height == iPhoneXRHeight || [[UIScreen mainScreen] nativeBounds].size.height == iPhoneXSMaxHeight || [[UIScreen mainScreen] nativeBounds].size.height == iPhoneXRScaledHeight || [[UIScreen mainScreen] nativeBounds].size.height == iPhone12 || [[UIScreen mainScreen] nativeBounds].size.height == iPhone12ProMax || [[UIScreen mainScreen] nativeBounds].size.height == iPhone12Mini); } + (UIImage *)imageNamed:(NSString *)name bundle:(Class)bundleClass channel:(ABKChannel)channel { NSBundle *bundle = [ABKUIUtils bundle:bundleClass channel:channel]; return [UIImage imageNamed:name inBundle:bundle compatibleWithTraitCollection:nil]; } + (UIInterfaceOrientation)getInterfaceOrientation { if (@available(iOS 13.0, *)) { UIWindowScene *windowScene = ABKUIUtils.activeWindowScene; if (windowScene) { return windowScene.interfaceOrientation; } } return UIApplication.sharedApplication.statusBarOrientation; } + (CGSize)getStatusBarSize { if (@available(iOS 13.0, *)) { UIWindowScene *windowScene = ABKUIUtils.activeWindowScene; if (windowScene) { return windowScene.statusBarManager.statusBarFrame.size; } } return UIApplication.sharedApplication.statusBarFrame.size; } #pragma mark - Dark Theme + (UIColor *)dynamicColorForLightColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor { if (lightColor == nil || darkColor == nil) { return lightColor; } #if !TARGET_OS_TV if (@available(iOS 13.0, *)) { // Crashes if either darkColor or lightColor is nil return [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) { if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) { return darkColor; } else { return lightColor; } }]; } else { return lightColor; } #else return lightColor; #endif } /*! * Unlike NSString's :isEqualToString method, this method returns true rather than throwing an exception if the first or both inputs are nil * OR the first or both inputs are NSNull. */ + (BOOL)string:(NSString *)string1 isEqualToString:(NSString *)string2 { if ((string1 == nil && string2 == nil) || ([string1 isKindOfClass:[NSNull class]] && [string2 isKindOfClass:[NSNull class]])) { return YES; } if (string1 == nil || [string1 isKindOfClass:[NSNull class]] || string2 == nil || [string2 isKindOfClass:[NSNull class]]) { return NO; } return [string1 isEqualToString:string2]; } + (BOOL)responderChainOf:(UIResponder *)responder hasKindOfClass:(Class)aClass { UIResponder *resp = responder; while (resp && ![resp isKindOfClass:aClass]) { resp = resp.nextResponder; } return resp != nil; } + (BOOL)responderChainOf:(UIResponder *)responder hasClassPrefixedWith:(NSString *)prefix { UIResponder *resp = responder; while (resp && ![NSStringFromClass(resp.class) hasPrefix:prefix]) { resp = resp.nextResponder; } return resp != nil; } + (UIFont *)preferredFontForTextStyle:(UIFontTextStyle)textStyle weight:(UIFontWeight)weight { if (@available(iOS 11.0, tvOS 11.0, *)) { UIFontMetrics *metrics = [UIFontMetrics metricsForTextStyle:textStyle]; UIFontDescriptor *descriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:textStyle]; UIFont *font = [UIFont systemFontOfSize:descriptor.pointSize weight:weight]; return [metrics scaledFontForFont:font]; } else { // https://apple.co/3snncd9 (Large / Default) static dispatch_once_t once; static NSDictionary *textStyleMap; dispatch_once(&once, ^{ textStyleMap = @{ UIFontTextStyleTitle1: @(28.0), UIFontTextStyleTitle2: @(22.0), UIFontTextStyleTitle3: @(20.0), UIFontTextStyleHeadline: @(17.0), UIFontTextStyleBody: @(17.0), UIFontTextStyleCallout: @(16.0), UIFontTextStyleSubheadline: @(15.0), UIFontTextStyleFootnote: @(13.0), UIFontTextStyleCaption1: @(12.0), UIFontTextStyleCaption2: @(11.0) }; }); return [UIFont systemFontOfSize:[textStyleMap[textStyle] doubleValue] weight:weight]; } } + (void)enableAdjustsFontForContentSizeCategory:(id)label { if (@available(iOS 10.0, tvOS 10.0, *)) { id adjustableLabel = label; if ([adjustableLabel respondsToSelector:@selector(setAdjustsFontForContentSizeCategory:)]) { adjustableLabel.adjustsFontForContentSizeCategory = YES; } } } @end ================================================ FILE: CHANGELOG.md ================================================ #### ⚠️ The New Braze [Swift SDK](https://github.com/braze-inc/braze-swift-sdk) is now available! ## 4.7.0 #### Breaking - Updates the minimum required version of SDWebImage from 5.8.2 to [5.18.7](https://github.com/SDWebImage/SDWebImage/releases/tag/5.18.7). - This version includes the privacy manifest for SDWebImage, which appears on the [privacy-impacting SDKs list](https://developer.apple.com/support/third-party-SDK-requirements/). #### Added - Adds the privacy manifest to describe data usage collected by Braze. For more details, refer to the [Apple documentation on privacy manifests](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files). - Adds code signatures to all XCFrameworks in the Braze iOS SDK, signed by `Braze, Inc.`. ##### Fixed - Fixes an issue in Full or Modal in-app messages where the header text would be duplicated in place of the body text under certain conditions. ## 4.6.0 This release requires Xcode `14.x`. #### Breaking - Drops support for iOS 9 and iOS 10. - Removes support for the outdated `.framework` assets when importing via Carthage in favor of the modern `.xcframework` assets. - Use the command `carthage update --use-xcframeworks` to import the appropriate Braze asset. - Removes support for `appboy_ios_sdk_full.json` in favor of using `appboy_ios_sdk.json` by including these lines in your `Cartfile`: ``` binary "https://raw.githubusercontent.com/Appboy/appboy-ios-sdk/master/appboy_ios_sdk.json" github "SDWebImage/SDWebImage" ``` ##### Fixed - Improves resilience when triggering in-app messages with date property filters. ##### Added - Adds a new option `ABKReenqueueInAppMessage` to enum `ABKInAppMessageDisplayChoice`. - Return this option in `beforeInAppMessageDisplayed:` of an `ABKInAppMessageControllerDelegate` to ensure that an in-app message is not displayed and becomes eligible to trigger again. - This option will reset any trigger times and re-eligibility rules as if it was never triggered. It will not add the message to the in-app message stack. ## 4.5.4 ##### Fixed - Improves reliability of custom event property type validation. - Fixes an issue where the status bar would not restore to its original state after a full in-app message was dismissed. ## 4.5.3 ##### Fixed - Fixes a crash that occurs when receiving custom event properties of numeric types under certain conditions. - Fixes UI responsiveness warnings when requesting location authorization status. ## 4.5.2 ##### Fixed - Improves reliability when validating trigger properties. - Improves the `NSURLSessionConfiguration` disk and memory cache capacities for file downloads. This change enables larger file downloads to be cached if needed. ## 4.5.1 ##### Fixed - Improves eligibility checks around the minimum trigger timeout for in-app messages by now checking at _trigger time_ in addition to _display time_. - Fixes an issue where purchases would not trigger certain templated in-app messages. ##### Added - Adds the delegate method `noMatchingTriggerForEvent:name:` to `ABKInAppMessageControllerDelegate`, which is called if no Braze in-app message was triggered for a given event. ## 4.5.0 ##### Added - Adds support for Content Cards to evaluate Retry-After headers. ## 4.4.4 ##### Fixed - Calling `appboyBridge.closeMessage()` or `brazeBridge.closeMessage()` from an HTML in-app message now correctly triggers `ABKInAppMessageUIDelegate.onInAppMessageDismissed:` when implemented. - Fixes an issue in `4.4.3` where the tvOS SDK incorrectly referenced an older SDK version. ## 4.4.3 ##### Fixed - Fixes an issue introduced in `4.4.0` which prevented custom events or purchases with an empty dictionary of properties from being logged. - Improves handling of `ABKInAppMessageWindow`'s dismissal to promptly remove it from the view hierarchy. - Fixes the position of the pinned indicator for _Captioned Image_ Content Cards when using the default UI. - Fixes an issue introduced in `4.3.2` and limited to users of `Appboy-tvOS-SDK`, which prevented custom events with properties or purchases with properties from being logged. ##### Added - Adds a `padding` property to `ABKCaptionedImageContentCardCell` to support modifying the default value. ## 4.4.2 ##### Fixed - Fixes a bug for HTML in-app messages using the _HTML Upload with Preview_ option to improve the reliability of in-app message display. - Fixes a bug preventing integration via Swift Package Manager in specific contexts. - Fixes an issue in the default Content Cards UI where the empty feed label was truncated if it was too large for the screen, for example due to accessibility or localization. - Fixes an issue where Slideup in-app messages would be automatically dismissed after multiple interaction with the app's main window. ##### Changed - If `changeUser:sdkAuthSignature:` is called with the current user's ID, but with a new and valid SDK Authentication signature, the new signature will be used. - Improves push tracking accuracy for apps making use of `UISceneDelegate` (UIKit) or `Scene` (SwiftUI). ## 4.4.1 ##### Fixed - Fixes an issue in which `input` elements with `type="date"` in HTML in-app messages do not respond to some user interactions on iOS 14 and iOS 15. - Fixes `ABKSdkMetadata` availability when using the dynamic variant of the SDK. - Fixes an issue in which the default content cards UI's empty feed label does not wrap properly when the device is using Larger Accessibility Sizes for its text size. ##### Changed - Changed `ABKInAppMessageUIDelegate.inAppMessageViewControllerWithInAppMessage:` to accept a `nil` return value. ##### Added - Adds support for the `playsinline` attribute on HTML `