Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ extension Storage.GeneralAppSettings {
installationDate: NullableCopiableProp<Date> = .copy,
feedbacks: CopiableProp<[FeedbackType: FeedbackSettings]> = .copy,
isViewAddOnsSwitchEnabled: CopiableProp<Bool> = .copy,
isApplicationPasswordsSwitchEnabled: CopiableProp<Bool> = .copy,
knownCardReaders: CopiableProp<[String]> = .copy,
lastEligibilityErrorInfo: NullableCopiableProp<EligibilityErrorInfo> = .copy,
lastJetpackBenefitsBannerDismissedTime: NullableCopiableProp<Date> = .copy,
Expand All @@ -68,6 +69,7 @@ extension Storage.GeneralAppSettings {
let installationDate = installationDate ?? self.installationDate
let feedbacks = feedbacks ?? self.feedbacks
let isViewAddOnsSwitchEnabled = isViewAddOnsSwitchEnabled ?? self.isViewAddOnsSwitchEnabled
let isApplicationPasswordsSwitchEnabled = isApplicationPasswordsSwitchEnabled ?? self.isApplicationPasswordsSwitchEnabled
let knownCardReaders = knownCardReaders ?? self.knownCardReaders
let lastEligibilityErrorInfo = lastEligibilityErrorInfo ?? self.lastEligibilityErrorInfo
let lastJetpackBenefitsBannerDismissedTime = lastJetpackBenefitsBannerDismissedTime ?? self.lastJetpackBenefitsBannerDismissedTime
Expand All @@ -80,6 +82,7 @@ extension Storage.GeneralAppSettings {
installationDate: installationDate,
feedbacks: feedbacks,
isViewAddOnsSwitchEnabled: isViewAddOnsSwitchEnabled,
isApplicationPasswordsSwitchEnabled: isApplicationPasswordsSwitchEnabled,
knownCardReaders: knownCardReaders,
lastEligibilityErrorInfo: lastEligibilityErrorInfo,
lastJetpackBenefitsBannerDismissedTime: lastJetpackBenefitsBannerDismissedTime,
Expand Down
10 changes: 10 additions & 0 deletions Modules/Sources/Storage/Model/GeneralAppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ public struct GeneralAppSettings: Codable, Equatable, GeneratedCopiable {
///
public var isViewAddOnsSwitchEnabled: Bool

/// The state(`true` or `false`) for the application passwords feature switch.
///
public var isApplicationPasswordsSwitchEnabled: Bool

/// A list (possibly empty) of known card reader IDs - i.e. IDs of card readers that should be reconnected to automatically
/// e.g. ["CHB204909005931"]
///
Expand Down Expand Up @@ -55,6 +59,7 @@ public struct GeneralAppSettings: Codable, Equatable, GeneratedCopiable {
public init(installationDate: Date?,
feedbacks: [FeedbackType: FeedbackSettings],
isViewAddOnsSwitchEnabled: Bool,
isApplicationPasswordsSwitchEnabled: Bool,
knownCardReaders: [String],
lastEligibilityErrorInfo: EligibilityErrorInfo? = nil,
lastJetpackBenefitsBannerDismissedTime: Date? = nil,
Expand All @@ -65,6 +70,7 @@ public struct GeneralAppSettings: Codable, Equatable, GeneratedCopiable {
self.installationDate = installationDate
self.feedbacks = feedbacks
self.isViewAddOnsSwitchEnabled = isViewAddOnsSwitchEnabled
self.isApplicationPasswordsSwitchEnabled = isApplicationPasswordsSwitchEnabled
self.knownCardReaders = knownCardReaders
self.lastEligibilityErrorInfo = lastEligibilityErrorInfo
self.lastJetpackBenefitsBannerDismissedTime = lastJetpackBenefitsBannerDismissedTime
Expand All @@ -78,6 +84,7 @@ public struct GeneralAppSettings: Codable, Equatable, GeneratedCopiable {
.init(installationDate: nil,
feedbacks: [:],
isViewAddOnsSwitchEnabled: false,
isApplicationPasswordsSwitchEnabled: true,
knownCardReaders: [],
lastEligibilityErrorInfo: nil,
featureAnnouncementCampaignSettings: [:],
Expand Down Expand Up @@ -107,6 +114,7 @@ public struct GeneralAppSettings: Codable, Equatable, GeneratedCopiable {
installationDate: installationDate,
feedbacks: updatedFeedbacks,
isViewAddOnsSwitchEnabled: isViewAddOnsSwitchEnabled,
isApplicationPasswordsSwitchEnabled: isApplicationPasswordsSwitchEnabled,
knownCardReaders: knownCardReaders,
lastEligibilityErrorInfo: lastEligibilityErrorInfo,
featureAnnouncementCampaignSettings: featureAnnouncementCampaignSettings,
Expand All @@ -127,6 +135,7 @@ public struct GeneralAppSettings: Codable, Equatable, GeneratedCopiable {
installationDate: installationDate,
feedbacks: feedbacks,
isViewAddOnsSwitchEnabled: isViewAddOnsSwitchEnabled,
isApplicationPasswordsSwitchEnabled: isApplicationPasswordsSwitchEnabled,
knownCardReaders: knownCardReaders,
lastEligibilityErrorInfo: lastEligibilityErrorInfo,
featureAnnouncementCampaignSettings: updatedSettings,
Expand All @@ -147,6 +156,7 @@ extension GeneralAppSettings {
self.installationDate = try container.decodeIfPresent(Date.self, forKey: .installationDate)
self.feedbacks = try container.decodeIfPresent([FeedbackType: FeedbackSettings].self, forKey: .feedbacks) ?? [:]
self.isViewAddOnsSwitchEnabled = try container.decodeIfPresent(Bool.self, forKey: .isViewAddOnsSwitchEnabled) ?? false
self.isApplicationPasswordsSwitchEnabled = try container.decodeIfPresent(Bool.self, forKey: .isApplicationPasswordsSwitchEnabled) ?? false
self.knownCardReaders = try container.decodeIfPresent([String].self, forKey: .knownCardReaders) ?? []
self.lastEligibilityErrorInfo = try container.decodeIfPresent(EligibilityErrorInfo.self, forKey: .lastEligibilityErrorInfo)
self.lastJetpackBenefitsBannerDismissedTime = try container.decodeIfPresent(Date.self, forKey: .lastJetpackBenefitsBannerDismissedTime)
Expand Down
10 changes: 10 additions & 0 deletions Modules/Sources/Yosemite/Actions/AppSettingsAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -356,4 +356,14 @@ public enum AppSettingsAction: Action {
/// Loads the favorite products.
///
case loadFavoriteProductIDs(siteID: Int64, onCompletion: ([Int64]) -> Void)

// MARK: - Application passwords Experiment feature

/// Sets the state of the App Passwords Experiment feature
///
case setAppPasswordsExperimentSettingState(isOn: Bool, onCompletion: (Result<Void, Error>) -> Void)

/// Loads Loads the state of the App Passwords Experiment feature
///
case getAppPasswordsExperimentSettingState(onCompletion: (Bool) -> Void)
}
21 changes: 21 additions & 0 deletions Modules/Sources/Yosemite/Stores/AppSettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,10 @@ public class AppSettingsStore: Store {
dismissCustomFieldsTopBanner(onCompletion: onCompletion)
case .loadCustomFieldsTopBannerDismissState(let onCompletion):
loadCustomFieldsTopBannerDismissState(onCompletion: onCompletion)
case .setAppPasswordsExperimentSettingState(let value, let onCompletion):
setAppPasswordsExperimentSettingEnabled(isOn: value, onCompletion: onCompletion)
case .getAppPasswordsExperimentSettingState(let onCompletion):
getAppPasswordsExperimentSettingEnabled(onCompletion: onCompletion)
}
}
}
Expand Down Expand Up @@ -1269,6 +1273,23 @@ private extension AppSettingsStore {
}
}

// MARK: - Application Passwords Experiment Feature
//
private extension AppSettingsStore {
func setAppPasswordsExperimentSettingEnabled(isOn: Bool, onCompletion: (Result<Void, Error>) -> Void) {
do {
try generalAppSettings.setValue(isOn, for: \.isApplicationPasswordsSwitchEnabled)
onCompletion(.success(()))
} catch {
onCompletion(.failure(error))
}
}

func getAppPasswordsExperimentSettingEnabled(onCompletion: (Bool) -> Void) {
onCompletion(generalAppSettings.value(for: \.isApplicationPasswordsSwitchEnabled))
}
}

// MARK: - Errors

/// Errors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ final class GeneralAppSettingsTests: XCTestCase {
let previousSettings = GeneralAppSettings(installationDate: installationDate,
feedbacks: feedbackSettings,
isViewAddOnsSwitchEnabled: true,
isApplicationPasswordsSwitchEnabled: false,
knownCardReaders: readers,
lastEligibilityErrorInfo: eligibilityInfo,
lastJetpackBenefitsBannerDismissedTime: jetpackBannerDismissedDate,
Expand Down Expand Up @@ -114,6 +115,7 @@ private extension GeneralAppSettingsTests {
GeneralAppSettings(installationDate: installationDate,
feedbacks: feedbacks,
isViewAddOnsSwitchEnabled: isViewAddOnsSwitchEnabled,
isApplicationPasswordsSwitchEnabled: false,
knownCardReaders: knownCardReaders,
lastEligibilityErrorInfo: lastEligibilityErrorInfo,
lastJetpackBenefitsBannerDismissedTime: lastJetpackBenefitsBannerDismissedTime,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ private extension InAppFeedbackCardVisibilityUseCaseTests {
installationDate: installationDate,
feedbacks: [feedback.name: feedback],
isViewAddOnsSwitchEnabled: false,
isApplicationPasswordsSwitchEnabled: false,
knownCardReaders: [],
featureAnnouncementCampaignSettings: [:],
sitesWithAtLeastOneIPPTransactionFinished: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1522,6 +1522,7 @@ private extension AppSettingsStoreTests {
installationDate: installationDate,
feedbacks: [feedback.name: feedback],
isViewAddOnsSwitchEnabled: false,
isApplicationPasswordsSwitchEnabled: false,
knownCardReaders: [],
featureAnnouncementCampaignSettings: [:],
sitesWithAtLeastOneIPPTransactionFinished: [],
Expand All @@ -1536,6 +1537,7 @@ private extension AppSettingsStoreTests {
installationDate: Date(),
feedbacks: [:],
isViewAddOnsSwitchEnabled: false,
isApplicationPasswordsSwitchEnabled: false,
knownCardReaders: [],
featureAnnouncementCampaignSettings: featureAnnouncementCampaignSettings,
sitesWithAtLeastOneIPPTransactionFinished: [],
Expand Down
2 changes: 1 addition & 1 deletion RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

23.2
-----

- [*] Adds a toggle for Application Passwords feature in Experimental Features settings screen [https://github.com/woocommerce/woocommerce-ios/pull/16059]
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: since the feature is not enabled yet, this will not be available to users in version 23.2. Let's remove this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reverted in 9026d48


23.1
-----
Expand Down
1 change: 1 addition & 0 deletions WooCommerce/Classes/Analytics/WooAnalyticsStat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ enum WooAnalyticsStat: String {
case settingsBetaFeaturesButtonTapped = "settings_beta_features_button_tapped"
case settingsBetaFeaturesProductsToggled = "settings_beta_features_products_toggled"
case settingsBetaFeaturesOrderAddOnsToggled = "settings_beta_features_order_addons_toggled"
case settingsBetaFeaturesApplicationPasswordsToggled = "settings_beta_features_application_passwords_toggled"
case settingsBetaFeatureToggled = "settings_beta_feature_toggled"

case settingsPrivacySettingsTapped = "settings_privacy_settings_button_tapped"
Expand Down
13 changes: 10 additions & 3 deletions WooCommerce/Classes/Extensions/AttributedString+Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@ extension AttributedString {
static func withEmbeddedLink(
mainContent: String,
linkText: String,
link: String
link: String,
font: Font? = .body,
foregroundColor: Color? = Color(uiColor: .text)
) -> AttributedString {
let content = String.localizedStringWithFormat(mainContent, linkText)
var attributedText = AttributedString(content)
attributedText.font = .body
attributedText.foregroundColor = Color(uiColor: .text)

if let font {
attributedText.font = font
}
if let foregroundColor {
attributedText.foregroundColor = foregroundColor
}

if let range = attributedText.range(of: linkText),
let url = URL(string: link) {
Expand Down
3 changes: 3 additions & 0 deletions WooCommerce/Classes/Extensions/UserDefaults+Woo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ extension UserDefaults {

// Hide stores from store picker
case hiddenStoreIDs

// Application passwords experiment remote FF cached value
case applicationPasswordsExperimentRemoteFFValue
Copy link
Contributor

Choose a reason for hiding this comment

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

Another suggestion: should we clear this flag in SessionManager.reset?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in a085407

}
}

Expand Down
50 changes: 50 additions & 0 deletions WooCommerce/Classes/Model/BetaFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,34 @@ import protocol WooFoundation.WooAnalyticsEventPropertyType

enum BetaFeature: String, CaseIterable {
case viewAddOns
case applicationPasswords
}

extension BetaFeature {
var title: String {
switch self {
case .viewAddOns:
return Localization.viewAddOnsTitle
case .applicationPasswords:
return Localization.applicationPasswordsTitle
}
}

var description: String {
switch self {
case .viewAddOns:
return Localization.viewAddOnsDescription
case .applicationPasswords:
return Localization.applicationPasswordsDescription
}
}

var settingsKey: WritableKeyPath<GeneralAppSettings, Bool> {
switch self {
case .viewAddOns:
return \.isViewAddOnsSwitchEnabled
case .applicationPasswords:
return \.isApplicationPasswordsSwitchEnabled
}
}

Expand All @@ -35,6 +42,8 @@ extension BetaFeature {
switch self {
case .viewAddOns:
return .settingsBetaFeaturesOrderAddOnsToggled
case .applicationPasswords:
return .settingsBetaFeaturesApplicationPasswordsToggled
}
}

Expand All @@ -47,6 +56,26 @@ extension BetaFeature {
}
}

extension BetaFeature {
typealias DescriptionLink = (text: String, url: URL)

var descriptionLink: DescriptionLink? {
switch self {
case .viewAddOns:
return nil
case .applicationPasswords:
guard let url = URL(string: Constants.applicationPasswordsDocURL) else {
return nil
}

return DescriptionLink(
text: Localization.applicationPasswordsDescriptionLinkText,
url: url
)
}
}
}

extension GeneralAppSettingsStorage {
func betaFeatureEnabled(_ feature: BetaFeature) -> Bool {
value(for: feature.settingsKey)
Expand Down Expand Up @@ -86,5 +115,26 @@ private extension BetaFeature {
static let viewAddOnsDescription = NSLocalizedString(
"Test out viewing Order Add-Ons as we get ready to launch",
comment: "Cell description on the beta features screen to enable the order add-ons feature")

static let applicationPasswordsTitle = NSLocalizedString(
"experimentalFeatures.applicationPasswords.title",
value: "Application Passwords",
comment: "Cell title on the beta features screen to enable the application passwords feature")
static let applicationPasswordsDescription = NSLocalizedString(
"experimentalFeatures.applicationPasswords.description",
value: "Enable %@ to let the app fetch data directly from your WooCommerce site rather than via Jetpack connections",
comment: "Cell description on the beta features screen to enable application passwords feature. The placeholder will be replaced by a link title."
)

static let applicationPasswordsDescriptionLinkText = NSLocalizedString(
"experimentalFeatures.applicationPasswords.description.linkText",
value: "Application Passwords",
comment: "Link text to open Application Passwords documentation page"
)
}

enum Constants {
static let applicationPasswordsDocURL =
"https://wordpress.com/support/security/two-step-authentication/application-specific-passwords/"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Foundation
import Yosemite

final class ApplicationPasswordsExperimentState {
private let stores: StoresManager
private let availabilityChecker: ApplicationPasswordsExperimentAvailabilityCheckerProtocol

init(
stores: StoresManager = ServiceLocator.stores,
availabilityChecker: ApplicationPasswordsExperimentAvailabilityCheckerProtocol = ApplicationPasswordsExperimentAvailabilityChecker()
) {
self.stores = stores
self.availabilityChecker = availabilityChecker
}

var isAvailableAndEnabled: Bool {
get async {
let isAvailable = await availabilityChecker.fetchAvailability()
let isEnabled = await isEnabled
return isAvailable && isEnabled
}
}

@MainActor
private var isEnabled: Bool {
get async {
return await withCheckedContinuation { continuation in
stores.dispatch(
AppSettingsAction.getAppPasswordsExperimentSettingState { isOn in
continuation.resume(with: .success(isOn))
}
)
}
}
}
}

protocol ApplicationPasswordsExperimentAvailabilityCheckerProtocol {
var cachedValue: Bool { get }
func fetchAvailability() async -> Bool
}

final class ApplicationPasswordsExperimentAvailabilityChecker: ApplicationPasswordsExperimentAvailabilityCheckerProtocol {
private let userDefaults: UserDefaults

init(userDefaults: UserDefaults = .standard) {
self.userDefaults = userDefaults
}

var cachedValue: Bool {
get {
userDefaults[.applicationPasswordsExperimentRemoteFFValue] ?? false
} set {
userDefaults[.applicationPasswordsExperimentRemoteFFValue] = newValue
}
}

func fetchAvailability() async -> Bool {
await withCheckedContinuation { continuation in
//TODO: - put the remote FF checking here
let mockResultValue = true

DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
continuation.resume(returning: mockResultValue)
}

cachedValue = mockResultValue
}
}
}
Loading