Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Push Primer feature using Custom Json (RMCCX-6702) #322

Merged
merged 15 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
## Changelog

### Unreleased
- Features:
- Added support for PushPrimer Campaign using CustomJson [RMCCX-6702]
- Prevent showing Push Priemr Campaign if Notificaation Permission was granted already [RMCCX-6707]
- Added CustomJson feature for RMC SDK users [RMCCX-6712]
- Redirect user to App Notification Permission Settings if permission was denied previously in Push Primer [RMCCX-6710]

### 8.3.0 (2024-05-13)
- Improvements:
Expand Down
25 changes: 17 additions & 8 deletions Sources/RInAppMessaging/CampaignsValidator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ internal struct CampaignsValidator: CampaignsValidatorType {
private let campaignRepository: CampaignRepositoryType
private let eventMatcher: EventMatcherType
private let triggerValidator = TriggerAttributesValidator.self
private let notificationCenter: UserNotificationCenter

init(campaignRepository: CampaignRepositoryType,
eventMatcher: EventMatcherType) {

eventMatcher: EventMatcherType,
notificationCenter: UserNotificationCenter) {
self.campaignRepository = campaignRepository
self.eventMatcher = eventMatcher
self.notificationCenter = notificationCenter
}

func validate(validatedCampaignHandler: (_ campaign: Campaign, _ events: Set<Event>) -> Void) {
Expand All @@ -42,8 +44,11 @@ internal struct CampaignsValidator: CampaignsValidatorType {
return
}

guard !(campaign.isPushPrimer && isNotificationsAllowed()) else {
return
// Enable PushPrimer only for RMC Sdk
if campaign.isPushPrimer {
guard RInAppMessaging.isRMCEnvironment, !isNotificationAuthorized() else {
return
}
}

guard let campaignTriggers = campaign.data.triggers else {
Expand Down Expand Up @@ -99,17 +104,21 @@ internal struct CampaignsValidator: CampaignsValidatorType {
return triggeredEvents
}

private func isNotificationsAllowed() -> Bool {
func isNotificationAuthorized() -> Bool {
let semaphore = DispatchSemaphore(value: 0)
var authorizationStatus: UNAuthorizationStatus = .notDetermined

let center = UNUserNotificationCenter.current()
center.getNotificationSettings { settings in
notificationCenter.getNotificationSettings { settings in
authorizationStatus = settings.authorizationStatus
semaphore.signal()
}
semaphore.wait()

return authorizationStatus == .authorized
switch authorizationStatus {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change this to previous implementation where it was relay on .authorized else it wouldn't work when status was provisional.

case .denied, .notDetermined :
return false
default :
return true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ internal enum MainContainerFactory {
elements.append(contentsOf: [
ContainerElement(type: CampaignsValidatorType.self, factory: {
CampaignsValidator(campaignRepository: manager.resolve(type: CampaignRepositoryType.self)!,
eventMatcher: manager.resolve(type: EventMatcherType.self)!)
eventMatcher: manager.resolve(type: EventMatcherType.self)!,
notificationCenter: UNUserNotificationService())
}, transient: true),
ContainerElement(type: FullViewPresenterType.self, factory: {
FullViewPresenter(campaignRepository: manager.resolve(type: CampaignRepositoryType.self)!,
Expand Down
12 changes: 6 additions & 6 deletions Sources/RInAppMessaging/Extensions/UIApplication+IAM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@ extension UIApplication {
}

private static let notificationSettingsURL: URL? = {
let urlString: String?
var urlString: String?

if #available(iOS 16, *) {
urlString = UIApplication.openNotificationSettingsURLString
} else if #available(iOS 15.4, *) {
}
if #available(iOS 15.4, *) {
urlString = UIApplicationOpenNotificationSettingsURLString
} else if #available(iOS 8.0, *) {
}
if #available(iOS 8.0, *) {
urlString = UIApplication.openSettingsURLString
} else {
urlString = nil
}

guard let urlString = urlString, let url = URL(string: urlString) else {
Expand All @@ -57,7 +57,7 @@ extension UIApplication {
}()

func openAppNotificationSettings() {
guard let url = Self.notificationSettingsURL, self.canOpenURL(url) else {
guard let url = Self.notificationSettingsURL else {
return
}
self.open(url)
SoumenRautray marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
2 changes: 1 addition & 1 deletion Sources/RInAppMessaging/Models/Responses/Campaign.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ internal struct Campaign: Codable, Hashable {
}.filter { !$0.isEmpty }
}
var isPushPrimer: Bool {
return RInAppMessaging.isRMCEnvironment && data.customJson?.pushPrimer?.button != nil
return data.customJson?.pushPrimer?.button != nil
}

init(from decoder: Decoder) throws {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,27 @@ extension UNUserNotificationCenter: RemoteNotificationRequestable {
}
}
}

protocol UserNotificationCenter {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make use of RemoteNotificationRequestable & follow the implementation of that

func getNotificationSettings(completionHandler: @escaping (NotificationSettingsProtocol) -> Void)
}

protocol NotificationSettingsProtocol {
var authorizationStatus: UNAuthorizationStatus { get }
}

class UNUserNotificationService: UserNotificationCenter {
private let center: UNUserNotificationCenter

init(center: UNUserNotificationCenter = UNUserNotificationCenter.current()) {
self.center = center
}

func getNotificationSettings(completionHandler: @escaping (NotificationSettingsProtocol) -> Void) {
center.getNotificationSettings { settings in
completionHandler(settings as NotificationSettingsProtocol)
}
}
}

extension UNNotificationSettings: NotificationSettingsProtocol {}
37 changes: 36 additions & 1 deletion Tests/Tests/CampaignsValidatorSpec.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Quick
import Nimble
import UserNotifications
@testable import RInAppMessaging

class CampaignsValidatorSpec: QuickSpec {
Expand All @@ -10,18 +11,21 @@ class CampaignsValidatorSpec: QuickSpec {
var campaignRepository: CampaignRepository!
var eventMatcher: EventMatcher!
var validatorHandler: ValidatorHandler!
var mockNotificationCenter: MockUserNotificationCenter!

func syncRepository(with campaigns: [Campaign]) {
campaignRepository.syncWith(list: campaigns, timestampMilliseconds: 0, ignoreTooltips: false)
}

beforeEach {
mockNotificationCenter = MockUserNotificationCenter()
campaignRepository = CampaignRepository(userDataCache: UserDataCacheMock(),
accountRepository: AccountRepository(userDataCache: UserDataCacheMock()))
eventMatcher = EventMatcher(campaignRepository: campaignRepository)
campaignsValidator = CampaignsValidator(
campaignRepository: campaignRepository,
eventMatcher: eventMatcher)
eventMatcher: eventMatcher,
notificationCenter: mockNotificationCenter)
validatorHandler = ValidatorHandler()
}

Expand Down Expand Up @@ -223,6 +227,37 @@ class CampaignsValidatorSpec: QuickSpec {
expect(validatorHandler.validatedCampaigns).to(contain(tooltip))
}
}
context("when notification is authorized") {
it("returns true") {
mockNotificationCenter.mockAuthorizationStatus = .authorized
let result = campaignsValidator.isNotificationAuthorized()
expect(result).to(beTrue())
}
}

context("when notification is denied") {
it("returns false") {
mockNotificationCenter.mockAuthorizationStatus = .denied
let result = campaignsValidator.isNotificationAuthorized()
expect(result).to(beFalse())
}
}

context("when notification is not determined") {
it("returns false") {
mockNotificationCenter.mockAuthorizationStatus = .notDetermined
let result = campaignsValidator.isNotificationAuthorized()
expect(result).to(beFalse())
}
}

context("when notification is provisional") {
it("returns false") {
mockNotificationCenter.mockAuthorizationStatus = .provisional
let result = campaignsValidator.isNotificationAuthorized()
expect(result).to(beTrue())
}
}
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion Tests/Tests/CustomEventValidationSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,21 @@ class CustomEventValidationSpec: QuickSpec {
var campaignRepository: CampaignRepository!
var eventMatcher: EventMatcher!
var validatorHandler: ValidatorHandler!
var mockCenter: MockUserNotificationCenter!

func syncRepository(with campaigns: [Campaign]) {
campaignRepository.syncWith(list: campaigns, timestampMilliseconds: 0, ignoreTooltips: false)
}

beforeEach {
mockCenter = MockUserNotificationCenter()
campaignRepository = CampaignRepository(userDataCache: UserDataCacheMock(),
accountRepository: AccountRepository(userDataCache: UserDataCacheMock()))
eventMatcher = EventMatcher(campaignRepository: campaignRepository)
campaignsValidator = CampaignsValidator(
campaignRepository: campaignRepository,
eventMatcher: eventMatcher)
eventMatcher: eventMatcher,
notificationCenter: mockCenter)
validatorHandler = ValidatorHandler()
}

Expand Down
17 changes: 17 additions & 0 deletions Tests/Tests/Helpers/SharedMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -696,3 +696,20 @@ extension EndpointURL {
impression: nil)
}
}

class MockNotificationSettings: NotificationSettingsProtocol {
var authorizationStatus: UNAuthorizationStatus

init(authorizationStatus: UNAuthorizationStatus) {
self.authorizationStatus = authorizationStatus
}
}

class MockUserNotificationCenter: UserNotificationCenter {
var mockAuthorizationStatus: UNAuthorizationStatus = .notDetermined

func getNotificationSettings(completionHandler: @escaping (NotificationSettingsProtocol) -> Void) {
let settings = MockNotificationSettings(authorizationStatus: mockAuthorizationStatus)
completionHandler(settings)
}
}
10 changes: 10 additions & 0 deletions Tests/Tests/Payloads/ping_success.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@
"attributes": []
}
],
"customJson": {
"pushPrimer": {
"button": 2
}
},
"messagePayload": {
"title": "Campaign with image test-1",
"messageBody": "body",
Expand Down Expand Up @@ -129,6 +134,11 @@
"attributes": []
}
],
"customJson": {
"pushPrimer": {
"button": 2
}
},
"messagePayload": {
"title": "Campaign with image test-1",
"messageBody": "body",
Expand Down
6 changes: 3 additions & 3 deletions Tests/Tests/ViewPresenterSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,7 @@ class ViewPresenterSpec: QuickSpec {
expect(impressionService.sentImpressions?.list).toNot(contain(.optOut))
}
}
context("when custom json data is available in Campaign Data") {
context("when push primer content is available available in Campaign Data") {
it("will replace the existing function and enable push primer function for the button") {
let campaignPrimer = TestHelpers.generateCampaign(id: "PushPrimer", buttons: [
Button(buttonText: "button1",
Expand All @@ -677,7 +677,7 @@ class ViewPresenterSpec: QuickSpec {
expect(view.addedButtons.map({ $0.0.type })).to(equal([ActionType.pushPrimer]))
}

it("will enable retain the button function if pushPrimer data in invalid in custom Json") {
it("will retain the button function if pushPrimer data in invalid") {
let campaignPrimer = TestHelpers.generateCampaign(id: "PushPrimer", buttons: [
Button(buttonText: "button1",
buttonTextColor: "#000000",
Expand All @@ -695,7 +695,7 @@ class ViewPresenterSpec: QuickSpec {
expect(view.addedButtons.map({ $0.0.type })).to(equal([ActionType.close]))
}

it("will enable retain the button function if push primer button value in invalid") {
it("will retain the button function if push primer button value is invalid") {
let campaignPrimer = TestHelpers.generateCampaign(id: "PushPrimer", buttons: [
Button(buttonText: "button1",
buttonTextColor: "#000000",
Expand Down
Loading