Skip to content

Commit b4147e3

Browse files
authored
[Woo POS][Surveys] Make PointOfSaleNotificationScheduler for initial eligibility schedule triggers (#16238)
2 parents 9cd02f4 + 5eafca4 commit b4147e3

File tree

4 files changed

+297
-0
lines changed

4 files changed

+297
-0
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import Foundation
2+
import UserNotifications
3+
import Yosemite
4+
import Experiments
5+
6+
// periphery: ignore - work in progress
7+
protocol POSNotificationScheduling {
8+
func scheduleLocalNotificationIfEligible(for merchantType: POSNotificationScheduler.MerchantType) async
9+
}
10+
11+
// periphery: ignore - work in progress
12+
final class POSNotificationScheduler: POSNotificationScheduling {
13+
enum MerchantType {
14+
case potentialMerchant
15+
case currentMerchant
16+
17+
var scenario: LocalNotification.Scenario {
18+
switch self {
19+
case .potentialMerchant:
20+
return .pointOfSalePotentialMerchant
21+
case .currentMerchant:
22+
return .pointOfSaleCurrentMerchant
23+
}
24+
}
25+
26+
var surveyURL: String {
27+
switch self {
28+
case .potentialMerchant:
29+
return LocalNotification.SurveyURL.pointOfSalePotentialMerchant
30+
case .currentMerchant:
31+
return LocalNotification.SurveyURL.pointOfSaleCurrentMerchant
32+
}
33+
}
34+
35+
var timeIntervalInSeconds: Int {
36+
switch self {
37+
case .potentialMerchant:
38+
return 60
39+
case .currentMerchant:
40+
return 60 * 5
41+
}
42+
}
43+
44+
var timeInterval: TimeInterval {
45+
TimeInterval(timeIntervalInSeconds)
46+
}
47+
}
48+
49+
private let siteSettings: [SiteSetting]
50+
private let featureFlagService: FeatureFlagService
51+
private let pushNotificationsManager: PushNotesManager
52+
53+
init(siteSettings: [SiteSetting] = ServiceLocator.selectedSiteSettings.siteSettings,
54+
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService,
55+
pushNotificationsManager: PushNotesManager = ServiceLocator.pushNotesManager) {
56+
self.siteSettings = siteSettings
57+
self.featureFlagService = featureFlagService
58+
self.pushNotificationsManager = pushNotificationsManager
59+
}
60+
61+
func scheduleLocalNotificationIfEligible(for merchantType: POSNotificationScheduler.MerchantType) async {
62+
// TODO: Additional check to see if .currentMerchant case has used POS before - WOOMOB-1498
63+
// TODO: Check as well if the notification hasn't been scheduled already WOOMOB-1461
64+
guard featureFlagService.isFeatureFlagEnabled(.pointOfSaleSurveys) else { return }
65+
guard isCountryEligible() else { return }
66+
67+
await scheduleLocalNotification(for: merchantType)
68+
}
69+
70+
private func isCountryEligible() -> Bool {
71+
let storeCountry = SiteAddress(siteSettings: siteSettings).countryCode
72+
if storeCountry == .US || storeCountry == .GB {
73+
return true
74+
} else {
75+
return false
76+
}
77+
}
78+
79+
private func scheduleLocalNotification(for merchantType: POSNotificationScheduler.MerchantType) async {
80+
// TODO: Set scheduled notification value in app storage - WOOMOB-1461
81+
let payload: [AnyHashable: Any] = [
82+
LocalNotification.UserInfoKey.surveyURL: merchantType.surveyURL
83+
]
84+
85+
let notification = LocalNotification(
86+
scenario: merchantType.scenario,
87+
userInfo: payload
88+
)
89+
90+
let trigger = UNTimeIntervalNotificationTrigger(
91+
timeInterval: merchantType.timeInterval,
92+
repeats: false
93+
)
94+
95+
await pushNotificationsManager.requestLocalNotification(notification, trigger: trigger)
96+
}
97+
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1340,6 +1340,7 @@
13401340
57CFCD2A2488496F003F51EC /* PrimarySectionHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 57CFCD292488496F003F51EC /* PrimarySectionHeaderView.xib */; };
13411341
57F2C6CD246DECC10074063B /* SummaryTableViewCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F2C6CC246DECC10074063B /* SummaryTableViewCellViewModelTests.swift */; };
13421342
57F42E40253768D600EA87F7 /* TitleAndEditableValueTableViewCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F42E3F253768D600EA87F7 /* TitleAndEditableValueTableViewCellViewModelTests.swift */; };
1343+
68051E1E2E9DFE5500228196 /* POSNotificationSchedulerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68051E1D2E9DFE5100228196 /* POSNotificationSchedulerTests.swift */; };
13431344
680BA59A2A4C377900F5559D /* UpgradeViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680BA5992A4C377900F5559D /* UpgradeViewState.swift */; };
13441345
680E36B52BD8B9B900E8BCEA /* OrderSubscriptionTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 680E36B42BD8B9B900E8BCEA /* OrderSubscriptionTableViewCell.xib */; };
13451346
680E36B72BD8C49F00E8BCEA /* OrderSubscriptionTableViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680E36B62BD8C49F00E8BCEA /* OrderSubscriptionTableViewCellViewModel.swift */; };
@@ -1350,6 +1351,7 @@
13501351
6832C7CC26DA5FDF00BA4088 /* LabeledTextViewTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6832C7CB26DA5FDE00BA4088 /* LabeledTextViewTableViewCell.xib */; };
13511352
683421642ACE9391009021D7 /* ProductDiscountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683421632ACE9391009021D7 /* ProductDiscountView.swift */; };
13521353
683AA9D62A303CB70099F7BA /* UpgradesViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683AA9D52A303CB70099F7BA /* UpgradesViewModelTests.swift */; };
1354+
683F18662E9CC839007BC608 /* POSNotificationScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683F18652E9CC838007BC608 /* POSNotificationScheduler.swift */; };
13531355
684AB83A2870677F003DFDD1 /* CardReaderManualsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684AB8392870677F003DFDD1 /* CardReaderManualsView.swift */; };
13541356
684AB83C2873DF04003DFDD1 /* CardReaderManualsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684AB83B2873DF04003DFDD1 /* CardReaderManualsViewModel.swift */; };
13551357
6850C5EE2B69E6580026A93B /* ReceiptViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6850C5ED2B69E6580026A93B /* ReceiptViewModel.swift */; };
@@ -4246,6 +4248,7 @@
42464248
57CFCD292488496F003F51EC /* PrimarySectionHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PrimarySectionHeaderView.xib; sourceTree = "<group>"; };
42474249
57F2C6CC246DECC10074063B /* SummaryTableViewCellViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryTableViewCellViewModelTests.swift; sourceTree = "<group>"; };
42484250
57F42E3F253768D600EA87F7 /* TitleAndEditableValueTableViewCellViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleAndEditableValueTableViewCellViewModelTests.swift; sourceTree = "<group>"; };
4251+
68051E1D2E9DFE5100228196 /* POSNotificationSchedulerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSNotificationSchedulerTests.swift; sourceTree = "<group>"; };
42494252
680BA5992A4C377900F5559D /* UpgradeViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradeViewState.swift; sourceTree = "<group>"; };
42504253
680E36B42BD8B9B900E8BCEA /* OrderSubscriptionTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = OrderSubscriptionTableViewCell.xib; sourceTree = "<group>"; };
42514254
680E36B62BD8C49F00E8BCEA /* OrderSubscriptionTableViewCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderSubscriptionTableViewCellViewModel.swift; sourceTree = "<group>"; };
@@ -4256,6 +4259,7 @@
42564259
6832C7CB26DA5FDE00BA4088 /* LabeledTextViewTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LabeledTextViewTableViewCell.xib; sourceTree = "<group>"; };
42574260
683421632ACE9391009021D7 /* ProductDiscountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDiscountView.swift; sourceTree = "<group>"; };
42584261
683AA9D52A303CB70099F7BA /* UpgradesViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradesViewModelTests.swift; sourceTree = "<group>"; };
4262+
683F18652E9CC838007BC608 /* POSNotificationScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSNotificationScheduler.swift; sourceTree = "<group>"; };
42594263
684AB8392870677F003DFDD1 /* CardReaderManualsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderManualsView.swift; sourceTree = "<group>"; };
42604264
684AB83B2873DF04003DFDD1 /* CardReaderManualsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderManualsViewModel.swift; sourceTree = "<group>"; };
42614265
6850C5ED2B69E6580026A93B /* ReceiptViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptViewModel.swift; sourceTree = "<group>"; };
@@ -6783,6 +6787,7 @@
67836787
029327662BF59D2D00D703E7 /* POS */ = {
67846788
isa = PBXGroup;
67856789
children = (
6790+
683F18652E9CC838007BC608 /* POSNotificationScheduler.swift */,
67866791
01654EB02E786223001DBB6F /* Adaptors */,
67876792
02ABF9B92DF7F8E200348186 /* TabBar */,
67886793
);
@@ -12032,6 +12037,7 @@
1203212037
DABF35242C11B40C006AF826 /* POS */ = {
1203312038
isa = PBXGroup;
1203412039
children = (
12040+
68051E1D2E9DFE5100228196 /* POSNotificationSchedulerTests.swift */,
1203512041
687C006C2D63469F00F832FC /* Analytics */,
1203612042
02CD3BFC2C35D01600E575C4 /* Mocks */,
1203712043
);
@@ -15130,6 +15136,7 @@
1513015136
B946880E29B627EB000646B0 /* SearchableActivityConvertable.swift in Sources */,
1513115137
010F7D8B2E79B763002B02EA /* POSCouponCreationSheetAdaptor.swift in Sources */,
1513215138
EE09DE0B2C2D6E5100A32680 /* SelectPackageImageCoordinator.swift in Sources */,
15139+
683F18662E9CC839007BC608 /* POSNotificationScheduler.swift in Sources */,
1513315140
DE621F6A29D67E1B000DE3BD /* WooAnalyticsEvent+JetpackSetup.swift in Sources */,
1513415141
DE78DE422B2813E4002E58DE /* ThemesCarouselViewModel.swift in Sources */,
1513515142
DEE183F1292E0ED0008818AB /* JetpackSetupInterruptedView.swift in Sources */,
@@ -15757,6 +15764,7 @@
1575715764
EE3E9E8C2B05B7D600985B2C /* SubscriptionTrialViewModelTests.swift in Sources */,
1575815765
B958A7D328B52A2300823EEF /* MockRoute.swift in Sources */,
1575915766
02153211242376B5003F2BBD /* ProductPriceSettingsViewModelTests.swift in Sources */,
15767+
68051E1E2E9DFE5500228196 /* POSNotificationSchedulerTests.swift in Sources */,
1576015768
45C8B25D231529410002FA77 /* CustomerInfoTableViewCellTests.swift in Sources */,
1576115769
035BA3A8291000E90056F0AD /* JustInTimeMessageViewModelTests.swift in Sources */,
1576215770
DE6627EB2DCCC3850068E12E /* ShippingLabelHelpersTests.swift in Sources */,

WooCommerce/WooCommerceTests/Mocks/MockPushNotificationsManager.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ final class MockPushNotificationsManager: PushNotesManager {
5656
private(set) var triggersForRequestedLocalNotificationsIfNeeded: [UNNotificationTrigger] = []
5757
private(set) var canceledLocalNotificationScenarios: [[LocalNotification.Scenario]] = []
5858
private(set) var resetBadgeCountKinds: [Note.Kind] = []
59+
var onRequestLocalNotificationCalled: (() -> Void)?
5960

6061
init(mockedDeviceID: String? = nil) {
6162
self.mockedDeviceID = mockedDeviceID
@@ -111,6 +112,7 @@ final class MockPushNotificationsManager: PushNotesManager {
111112
if let trigger {
112113
triggersForRequestedLocalNotifications.append(trigger)
113114
}
115+
onRequestLocalNotificationCalled?()
114116
}
115117
}
116118

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import Testing
2+
import UserNotifications
3+
@testable import WooCommerce
4+
@testable import Yosemite
5+
6+
struct POSNotificationSchedulerTests {
7+
private let mockFeatureFlagService: MockFeatureFlagService
8+
private let mockPushNotesManager: MockPushNotificationsManager
9+
10+
init() {
11+
mockFeatureFlagService = MockFeatureFlagService()
12+
mockPushNotesManager = MockPushNotificationsManager()
13+
}
14+
15+
@Test func scheduleLocalNotificationIfEligible_when_featureFlag_is_disabled_then_no_notification_scheduled() async throws {
16+
// Given
17+
let siteSettings = sampleSiteSettings(countryCode: "US")
18+
mockFeatureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleSurveys] = false
19+
20+
let scheduler = POSNotificationScheduler(
21+
siteSettings: siteSettings,
22+
featureFlagService: mockFeatureFlagService,
23+
pushNotificationsManager: mockPushNotesManager
24+
)
25+
26+
// When
27+
await scheduler.scheduleLocalNotificationIfEligible(for: .potentialMerchant)
28+
29+
// Then
30+
#expect(mockPushNotesManager.requestedLocalNotifications.isEmpty)
31+
}
32+
33+
@Test func scheduleLocalNotificationIfEligible_when_country_is_US_and_featureFlag_is_enabled_then_notification_is_scheduled() async throws {
34+
// Given
35+
let siteSettings = sampleSiteSettings(countryCode: "US")
36+
mockFeatureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleSurveys] = true
37+
38+
let scheduler = POSNotificationScheduler(
39+
siteSettings: siteSettings,
40+
featureFlagService: mockFeatureFlagService,
41+
pushNotificationsManager: mockPushNotesManager
42+
)
43+
44+
// When/Then
45+
await confirmation() { confirmation in
46+
mockPushNotesManager.onRequestLocalNotificationCalled = {
47+
confirmation()
48+
}
49+
await scheduler.scheduleLocalNotificationIfEligible(for: .potentialMerchant)
50+
}
51+
52+
#expect(mockPushNotesManager.requestedLocalNotifications.count == 1)
53+
}
54+
55+
@Test func scheduleLocalNotificationIfEligible_when_country_is_GB_and_featureFlag_is_enabled_then_notification_is_scheduled() async throws {
56+
// Given
57+
let siteSettings = sampleSiteSettings(countryCode: "GB")
58+
mockFeatureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleSurveys] = true
59+
60+
let scheduler = POSNotificationScheduler(
61+
siteSettings: siteSettings,
62+
featureFlagService: mockFeatureFlagService,
63+
pushNotificationsManager: mockPushNotesManager
64+
)
65+
66+
// When/Then
67+
await confirmation() { confirmation in
68+
mockPushNotesManager.onRequestLocalNotificationCalled = {
69+
confirmation()
70+
}
71+
await scheduler.scheduleLocalNotificationIfEligible(for: .potentialMerchant)
72+
}
73+
74+
#expect(mockPushNotesManager.requestedLocalNotifications.count == 1)
75+
}
76+
77+
@Test func scheduleLocalNotificationIfEligible_when_country_is_not_eligible_then_no_notification_scheduled() async throws {
78+
// Given
79+
let siteSettings = sampleSiteSettings(countryCode: "FR")
80+
mockFeatureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleSurveys] = true
81+
82+
let scheduler = POSNotificationScheduler(
83+
siteSettings: siteSettings,
84+
featureFlagService: mockFeatureFlagService,
85+
pushNotificationsManager: mockPushNotesManager
86+
)
87+
88+
// When
89+
await scheduler.scheduleLocalNotificationIfEligible(for: .potentialMerchant)
90+
91+
// Then
92+
#expect(mockPushNotesManager.requestedLocalNotifications.isEmpty)
93+
}
94+
95+
@Test func scheduleLocalNotificationIfEligible_when_potentialMerchant_case_then_uses_correct_scenario() async throws {
96+
// Given
97+
let siteSettings = sampleSiteSettings(countryCode: "US")
98+
mockFeatureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleSurveys] = true
99+
100+
let scheduler = POSNotificationScheduler(
101+
siteSettings: siteSettings,
102+
featureFlagService: mockFeatureFlagService,
103+
pushNotificationsManager: mockPushNotesManager
104+
)
105+
106+
// When/Then
107+
await confirmation() { confirmation in
108+
mockPushNotesManager.onRequestLocalNotificationCalled = {
109+
confirmation()
110+
}
111+
await scheduler.scheduleLocalNotificationIfEligible(for: .potentialMerchant)
112+
}
113+
114+
let notification = try #require(mockPushNotesManager.requestedLocalNotifications.first)
115+
let trigger = try #require(mockPushNotesManager.triggersForRequestedLocalNotifications.first as? UNTimeIntervalNotificationTrigger)
116+
117+
#expect(notification.scenario == .pointOfSalePotentialMerchant)
118+
#expect(notification.userInfo[LocalNotification.UserInfoKey.surveyURL] as? String ==
119+
LocalNotification.SurveyURL.pointOfSalePotentialMerchant)
120+
#expect(trigger.timeInterval == 60)
121+
#expect(trigger.repeats == false)
122+
}
123+
124+
@Test func scheduleLocalNotificationIfEligible_when_currentMerchant_case_then_uses_correct_scenario() async throws {
125+
// Given
126+
let siteSettings = sampleSiteSettings(countryCode: "US")
127+
mockFeatureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleSurveys] = true
128+
129+
let scheduler = POSNotificationScheduler(
130+
siteSettings: siteSettings,
131+
featureFlagService: mockFeatureFlagService,
132+
pushNotificationsManager: mockPushNotesManager
133+
)
134+
135+
// When/Then
136+
await confirmation() { confirmation in
137+
mockPushNotesManager.onRequestLocalNotificationCalled = {
138+
confirmation()
139+
}
140+
await scheduler.scheduleLocalNotificationIfEligible(for: .currentMerchant)
141+
}
142+
143+
let notification = try #require(mockPushNotesManager.requestedLocalNotifications.first)
144+
let trigger = try #require(mockPushNotesManager.triggersForRequestedLocalNotifications.first as? UNTimeIntervalNotificationTrigger)
145+
146+
#expect(notification.scenario == .pointOfSaleCurrentMerchant)
147+
#expect(notification.userInfo[LocalNotification.UserInfoKey.surveyURL] as? String ==
148+
LocalNotification.SurveyURL.pointOfSaleCurrentMerchant)
149+
#expect(trigger.timeInterval == 300)
150+
#expect(trigger.repeats == false)
151+
}
152+
153+
@Test func scheduleLocalNotificationIfEligible_when_eligible_then_notification_manager_receives_correct_notification() async throws {
154+
// Given
155+
let siteSettings = sampleSiteSettings(countryCode: "GB")
156+
mockFeatureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleSurveys] = true
157+
158+
let scheduler = POSNotificationScheduler(
159+
siteSettings: siteSettings,
160+
featureFlagService: mockFeatureFlagService,
161+
pushNotificationsManager: mockPushNotesManager
162+
)
163+
164+
// When/Then
165+
await confirmation() { confirmation in
166+
mockPushNotesManager.onRequestLocalNotificationCalled = {
167+
confirmation()
168+
}
169+
await scheduler.scheduleLocalNotificationIfEligible(for: .potentialMerchant)
170+
}
171+
172+
#expect(mockPushNotesManager.requestedLocalNotifications.count == 1)
173+
#expect(mockPushNotesManager.triggersForRequestedLocalNotifications.count == 1)
174+
175+
let notification = try #require(mockPushNotesManager.requestedLocalNotifications.first)
176+
#expect(notification.scenario == .pointOfSalePotentialMerchant)
177+
#expect(notification.userInfo.keys.contains(LocalNotification.UserInfoKey.surveyURL))
178+
}
179+
180+
private func sampleSiteSettings(countryCode: String) -> [SiteSetting] {
181+
[
182+
SiteSetting.fake().copy(
183+
siteID: 123,
184+
settingID: "woocommerce_default_country",
185+
value: countryCode,
186+
settingGroupKey: SiteSettingGroup.general.rawValue
187+
)
188+
]
189+
}
190+
}

0 commit comments

Comments
 (0)