deep-linking-sample/Apps/Wikipedia/WMF Framework/SurveyAnnouncementsController.swift
Javier Cicchelli 9bcdaa697b [Setup] Basic project structure (#1)
This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended.

To summarise this work:
- [x] created a new **Xcode** project;
- [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project;
- [x] created the `Locations` app and also, its `Libraries` package;
- [x] created the `Shared` package to share dependencies between the apps;
- [x] added a `Makefile` file and implemented some **environment** and **help** commands.

Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Reviewed-on: rock-n-code/deep-linking-assignment#1
2023-04-08 18:37:13 +00:00

120 lines
4.9 KiB
Swift

import Foundation
@objc(WMFSurveyAnnouncementsController)
public final class SurveyAnnouncementsController: NSObject {
@objc public static let shared = SurveyAnnouncementsController()
private let queue = DispatchQueue(label: "SurveyAnnouncementsQueue")
// ex: 'en.wikipedia.org'
typealias AnnouncementsHost = String
private var announcementsByHost: [AnnouncementsHost: [WMFAnnouncement]] = [:]
public struct SurveyAnnouncementResult {
public let campaignIdentifier: String
public let announcement: WMFAnnouncement
public let actionURLString: String
public let displayDelay: TimeInterval
}
public private(set) var failureDeterminingABTestBucket = false
@objc public func setAnnouncements(_ announcements: [WMFAnnouncement], forSiteURL siteURL: URL, dataStore: MWKDataStore) {
guard let components = URLComponents(url: siteURL, resolvingAgainstBaseURL: false),
let host = components.host else {
return
}
let surveyAnnouncements = announcements.filter { $0.announcementType == .survey }
queue.sync {
announcementsByHost[host] = surveyAnnouncements
}
// assign and persist ab test bucket & percentage
// this works for now since we only have one experiment for this release but will likely need to change as we expand
if let articleAsLivingDocAnnouncement = surveyAnnouncements.first(where: { ($0.identifier?.hasPrefix("IOSAAALDSURVEY")) ?? false }),
let percentage = articleAsLivingDocAnnouncement.percentReceivingExperiment {
do {
if dataStore.abTestsController.percentageForExperiment(.articleAsLivingDoc) == nil {
try dataStore.abTestsController.setPercentage(percentage, forExperiment: .articleAsLivingDoc)
}
try dataStore.abTestsController.determineBucketForExperiment(.articleAsLivingDoc, withPercentage: percentage)
failureDeterminingABTestBucket = false
} catch {
failureDeterminingABTestBucket = true
}
}
}
private func getAnnouncementsForSiteURL(_ siteURL: URL) -> [WMFAnnouncement]? {
guard let components = URLComponents(url: siteURL, resolvingAgainstBaseURL: false),
let host = components.host else {
return nil
}
var announcements: [WMFAnnouncement]? = []
queue.sync {
announcements = announcementsByHost[host]
}
return announcements
}
// Use for determining whether to show user a survey prompt or not.
// Considers domain, campaign start/end dates, and whether articleURL is within allowlist of article titles in campaign
public func activeSurveyAnnouncementResultForArticleURL(_ articleURL: URL) -> SurveyAnnouncementResult? {
guard let articleTitle = articleURL.wmf_title?.denormalizedPageTitle, let siteURL = articleURL.wmf_site else {
return nil
}
guard let announcements = getAnnouncementsForSiteURL(siteURL) else {
return nil
}
for announcement in announcements {
guard let startTime = announcement.startTime,
let endTime = announcement.endTime,
let domain = announcement.domain,
let articleTitles = announcement.articleTitles,
let displayDelay = announcement.displayDelay,
let components = URLComponents(url: siteURL, resolvingAgainstBaseURL: false),
let host = components.host,
let identifier = announcement.identifier,
let normalizedArticleTitle = articleTitle.normalizedPageTitle,
let actionURLString = announcement.actionURLString else {
continue
}
let now = Date()
if now > startTime && now < endTime && host == domain, articleTitles.contains(normalizedArticleTitle) {
return SurveyAnnouncementResult(campaignIdentifier: identifier, announcement: announcement, actionURLString: actionURLString, displayDelay: displayDelay.doubleValue)
}
}
return nil
}
public func userHasSeenSurveyPrompt(forCampaignIdentifier identifier: String) -> Bool {
// Note any value indicates survey was seen.
// true = they tapped through to the Google survey, false = they dismissed the survey prompt.
guard UserDefaults.standard.object(forKey: identifier) == nil else {
return true
}
return false
}
public func markSurveyAnnouncementAnswer(_ answer: Bool, campaignIdentifier: String) {
UserDefaults.standard.setValue(answer, forKey: campaignIdentifier)
}
}