Skip to content

Commit

Permalink
Merge branch 'trunk' into issue/10259-dashboard-error-banner
Browse files Browse the repository at this point in the history
  • Loading branch information
rachelmcr committed Jul 20, 2023
2 parents fd8a89b + ad98753 commit 519ae01
Show file tree
Hide file tree
Showing 30 changed files with 654 additions and 161 deletions.
6 changes: 6 additions & 0 deletions Experiments/Experiments/ABTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ public enum ABTest: String, Codable, CaseIterable {
/// Experiment ref: pbxNRc-1QS-p2
case aaTestLoggedIn = "woocommerceios_explat_aa_test_logged_in_202212_v2"

/// A/B test for flow to add product from an image.
/// Experiment ref: pbxNRc-2MS-p2
case addProductFromImage = "woocommerceios_add_product_from_photo_202307"

/// A/A test to make sure there is no bias in the logged out state.
/// Experiment ref: pbxNRc-1S0-p2
case aaTestLoggedOut = "woocommerceios_explat_aa_test_logged_out_202212_v2"
Expand All @@ -27,6 +31,8 @@ public enum ABTest: String, Codable, CaseIterable {
switch self {
case .aaTestLoggedIn:
return .loggedIn
case .addProductFromImage:
return .loggedIn
case .aaTestLoggedOut:
return .loggedOut
// Mocks
Expand Down
2 changes: 0 additions & 2 deletions Experiments/Experiments/DefaultFeatureFlagService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,6 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
return true
case .freeTrialInAppPurchasesUpgradeM2:
return buildConfig == .localDeveloper || buildConfig == .alpha
case .addProductFromImage:
return (buildConfig == .localDeveloper || buildConfig == .alpha) && !isUITesting
case .ordersWithCouponsM4:
return true
default:
Expand Down
4 changes: 0 additions & 4 deletions Experiments/Experiments/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,6 @@ public enum FeatureFlag: Int {
///
case freeTrialInAppPurchasesUpgradeM2

/// A new flow to add product from an image.
///
case addProductFromImage

/// Enables the Milestone 4 of the Orders with Coupons project: Adding discounts to products
case ordersWithCouponsM4
}
8 changes: 8 additions & 0 deletions Networking/Networking.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@
24F98C602502EF8200F49B68 /* FeatureFlagRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F98C5F2502EF8200F49B68 /* FeatureFlagRemoteTests.swift */; };
24F98C622502EFF600F49B68 /* feature-flags-load-all.json in Resources */ = {isa = PBXBuildFile; fileRef = 24F98C612502EFF600F49B68 /* feature-flags-load-all.json */; };
261870782540A252006522A1 /* ShippingLineTax.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261870772540A252006522A1 /* ShippingLineTax.swift */; };
261C466B2A6738EE00734070 /* AppicationPasswordEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261C466A2A6738EE00734070 /* AppicationPasswordEncoder.swift */; };
261C466D2A67478800734070 /* ApplicationPasswordEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261C466C2A67478800734070 /* ApplicationPasswordEncoderTests.swift */; };
261CF1B4255AD6B30090D8D3 /* payment-gateway-list.json in Resources */ = {isa = PBXBuildFile; fileRef = 261CF1B3255AD6B30090D8D3 /* payment-gateway-list.json */; };
261CF1B8255AE62D0090D8D3 /* PaymentGatewayRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261CF1B7255AE62D0090D8D3 /* PaymentGatewayRemote.swift */; };
261CF1BC255AEE290090D8D3 /* PaymentsGatewayRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261CF1BB255AEE290090D8D3 /* PaymentsGatewayRemoteTests.swift */; };
Expand Down Expand Up @@ -1122,6 +1124,8 @@
24F98C5F2502EF8200F49B68 /* FeatureFlagRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlagRemoteTests.swift; sourceTree = "<group>"; };
24F98C612502EFF600F49B68 /* feature-flags-load-all.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "feature-flags-load-all.json"; sourceTree = "<group>"; };
261870772540A252006522A1 /* ShippingLineTax.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLineTax.swift; sourceTree = "<group>"; };
261C466A2A6738EE00734070 /* AppicationPasswordEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppicationPasswordEncoder.swift; sourceTree = "<group>"; };
261C466C2A67478800734070 /* ApplicationPasswordEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordEncoderTests.swift; sourceTree = "<group>"; };
261CF1B3255AD6B30090D8D3 /* payment-gateway-list.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "payment-gateway-list.json"; sourceTree = "<group>"; };
261CF1B7255AE62D0090D8D3 /* PaymentGatewayRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentGatewayRemote.swift; sourceTree = "<group>"; };
261CF1BB255AEE290090D8D3 /* PaymentsGatewayRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsGatewayRemoteTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3166,6 +3170,7 @@
EE54C89E2947782E00A9BF61 /* ApplicationPasswordUseCase.swift */,
DEEF8E6729C858AD00D47411 /* OneTimeApplicationPasswordUseCase.swift */,
EE71CC3C2951A8EA0074D908 /* ApplicationPasswordStorage.swift */,
261C466A2A6738EE00734070 /* AppicationPasswordEncoder.swift */,
EE99814D295AA7430074AE68 /* RequestAuthenticator.swift */,
EE99814F295AACE10074AE68 /* RequestConverter.swift */,
);
Expand Down Expand Up @@ -3233,6 +3238,7 @@
EE8DE431294B17CD005054E7 /* DefaultApplicationPasswordUseCaseTests.swift */,
EE76762E2962B85E000066FA /* RequestProcessorTests.swift */,
45C6D0E329B9F327009CE29C /* CookieNonceAuthenticatorTests.swift */,
261C466C2A67478800734070 /* ApplicationPasswordEncoderTests.swift */,
);
path = ApplicationPassword;
sourceTree = "<group>";
Expand Down Expand Up @@ -4083,6 +4089,7 @@
0359EA1727AAC7740048DE2D /* WCPayCardFunding.swift in Sources */,
D88E229425AC9B420023F3B1 /* OrderFeeTaxStatus.swift in Sources */,
B5DAEFF02180DD5A0002356A /* NotificationsRemote.swift in Sources */,
261C466B2A6738EE00734070 /* AppicationPasswordEncoder.swift in Sources */,
2665032A261F41510079A159 /* ProductAddOn.swift in Sources */,
020D07BE23D8570800FD9580 /* MediaListMapper.swift in Sources */,
0359EA1327AAC6D00048DE2D /* WCPayCardPaymentDetails.swift in Sources */,
Expand Down Expand Up @@ -4325,6 +4332,7 @@
45ED4F14239E8F2E004F1BE3 /* TaxClassRemoteTests.swift in Sources */,
D88D5A4D230BD010007B6E01 /* ProductReviewsRemoteTests.swift in Sources */,
261CF1BC255AEE290090D8D3 /* PaymentsGatewayRemoteTests.swift in Sources */,
261C466D2A67478800734070 /* ApplicationPasswordEncoderTests.swift in Sources */,
453305EF2459E46100264E50 /* SitePostsRemoteTests.swift in Sources */,
45C6D0E429B9F327009CE29C /* CookieNonceAuthenticatorTests.swift in Sources */,
24F98C602502EF8200F49B68 /* FeatureFlagRemoteTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Foundation

/// Utility class to encode the stored application password.
/// By default it uses the stored application password.
///
public struct ApplicationPasswordEncoder {

/// Password envelope.
///
private let passwordEnvelope: ApplicationPassword?

public init(passwordEnvelope: ApplicationPassword? = nil) {
self.passwordEnvelope = passwordEnvelope ?? ApplicationPasswordStorage().applicationPassword
}

/// Returns the application password on a base64 encoded format.
/// The output is ready to be used in the authentication header.
/// Returns `nil` if the password can't be encoded.
///
public func encodedPassword() -> String? {
guard let passwordEnvelope else {
return nil
}

let loginString = "\(passwordEnvelope.wpOrgUsername):\(passwordEnvelope.password.secretValue)"
guard let loginData = loginString.data(using: .utf8) else {
return nil
}

return loginData.base64EncodedString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,14 @@ private extension OneTimeApplicationPasswordUseCase {
}

func authenticateRequest(request: URLRequest) -> URLRequest {
guard let username = applicationPassword?.wpOrgUsername,
let password = applicationPassword?.password.secretValue else {
guard let applicationPassword else {
return request
}
var authenticatedRequest = request
authenticatedRequest.setValue("application/json", forHTTPHeaderField: "Accept")
authenticatedRequest.setValue(UserAgent.defaultUserAgent, forHTTPHeaderField: "User-Agent")

let loginString = "\(username):\(password)"

if let loginData = loginString.data(using: .utf8) {
let base64LoginString = loginData.base64EncodedString()
if let base64LoginString = ApplicationPasswordEncoder(passwordEnvelope: applicationPassword).encodedPassword() {
authenticatedRequest.setValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,7 @@ struct AuthenticatedRESTRequest: URLRequestConvertible {
authenticated.setValue("application/json", forHTTPHeaderField: "Accept")
authenticated.setValue(UserAgent.defaultUserAgent, forHTTPHeaderField: "User-Agent")

let username = applicationPassword.wpOrgUsername
let password = applicationPassword.password.secretValue
let loginString = "\(username):\(password)"

if let loginData = loginString.data(using: .utf8) {
let base64LoginString = loginData.base64EncodedString()
if let base64LoginString = ApplicationPasswordEncoder(passwordEnvelope: applicationPassword).encodedPassword() {
authenticated.setValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization")
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import XCTest
@testable import Networking

final class ApplicationPasswordEncoderTests: XCTestCase {

func test_nil_password_envelope_returns_nil_password() {
let encoder = ApplicationPasswordEncoder(passwordEnvelope: nil)
XCTAssertNil(encoder.encodedPassword())
}

func test_sample_password_envelope_returns_encoded_password() {
// Given
let envelope = ApplicationPassword(wpOrgUsername: "This", password: .init("is-a-test"), uuid: "")

// When
let encoder = ApplicationPasswordEncoder(passwordEnvelope: envelope)

// Then
let expected = "VGhpczppcy1hLXRlc3Q=" /// `This:is-a-test` encoded with https://www.base64encode.org/
XCTAssertEqual(encoder.encodedPassword(), expected)
}
}
2 changes: 2 additions & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
-----
- [Internal] Switched AI endpoint to be able to track and measure costs. [https://github.com/woocommerce/woocommerce-ios/pull/10218]
- [Internal] Media picker flow was refactored to support interactive dismissal for device photo picker and WordPress media picker sources. Affected flows: product form > images, and virtual product form > downloadable files. [https://github.com/woocommerce/woocommerce-ios/pull/10236]
- [*] Orders with Coupons: Users can now select a coupon from a list when adding it to an order. [https://github.com/woocommerce/woocommerce-ios/pull/10255]
- [Internal] Orders: Improved error message when orders can't be loaded due to a parsing (decoding) error. [https://github.com/woocommerce/woocommerce-ios/pull/10252]
- [**] Product discounts: Users can now add discounts to products when creating an order. [https://github.com/woocommerce/woocommerce-ios/pull/10244]
- [Internal] Fixed a bug preventing the "We couldn't load your data" error banner from appearing on the My store dashboard. [https://github.com/woocommerce/woocommerce-ios/pull/10262]

- [Internal] A new way to create a product from an image using AI is being A/B tested. [https://github.com/woocommerce/woocommerce-ios/pull/10253]

14.5
-----
Expand Down
4 changes: 4 additions & 0 deletions WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,10 @@ extension WooAnalyticsEvent {
WooAnalyticsEvent(statName: .orderCouponRemove, properties: [Keys.flow: flow.rawValue])
}

static func orderGoToCouponsButtonTapped() -> WooAnalyticsEvent {
WooAnalyticsEvent(statName: .orderGoToCouponsButtonTapped, properties: [:])
}

static func productDiscountAdd(type: FeeOrDiscountLineDetailsViewModel.FeeOrDiscountType) -> WooAnalyticsEvent {
WooAnalyticsEvent(statName: .orderProductDiscountAdd, properties: [Keys.type: type.rawValue])
}
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 @@ -407,6 +407,7 @@ public enum WooAnalyticsStat: String {
case orderFeeRemove = "order_fee_remove"
case orderCouponAdd = "order_coupon_add"
case orderCouponRemove = "order_coupon_remove"
case orderGoToCouponsButtonTapped = "order_go_to_coupons_button_tapped"
case orderProductDiscountAdd = "order_product_discount_add"
case orderProductDiscountRemove = "order_product_discount_remove"
case orderProductDiscountAddButtonTapped = "order_product_discount_add_button_tapped"
Expand Down
20 changes: 20 additions & 0 deletions WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import SwiftUI
import Yosemite

struct CouponListView: UIViewControllerRepresentable {
let siteID: Int64
let emptyStateActionTitle: String
let emptyStateAction: (() -> Void)
let onCouponSelected: ((Coupon) -> Void)

func makeUIViewController(context: Self.Context) -> CouponListViewController {
let viewController = CouponListViewController(siteID: siteID,
showFeedbackBannerIfAppropriate: false,
emptyStateActionTitle: emptyStateActionTitle,
emptyStateAction: emptyStateAction,
onCouponSelected: onCouponSelected)
return viewController
}

func updateUIViewController(_ uiViewController: CouponListViewController, context: Context) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,23 @@ final class CouponListViewController: UIViewController, GhostableViewController
private lazy var dataSource: UITableViewDiffableDataSource<Section, CouponListViewModel.CellViewModel> = makeDataSource()
private lazy var topBannerView: TopBannerView = createFeedbackBannerView()

var onDataLoaded: ((Bool) -> Void)?
var noResultConfig: EmptyStateViewController.Config?
var onCouponSelected: ((Coupon) -> Void)?

init(siteID: Int64, showFeedbackBannerIfAppropriate: Bool) {
private var onDataLoaded: ((Bool) -> Void)?
private let emptyStateAction: (() -> Void)
private let emptyStateActionTitle: String
private let onCouponSelected: ((Coupon) -> Void)

init(siteID: Int64,
showFeedbackBannerIfAppropriate: Bool,
emptyStateActionTitle: String,
onDataLoaded: ((Bool) -> Void)? = nil,
emptyStateAction: @escaping (() -> Void),
onCouponSelected: @escaping ((Coupon) -> Void)) {
self.siteID = siteID
self.viewModel = CouponListViewModel(siteID: siteID, showFeedbackBannerIfAppropriate: showFeedbackBannerIfAppropriate)
self.onDataLoaded = onDataLoaded
self.emptyStateAction = emptyStateAction
self.emptyStateActionTitle = emptyStateActionTitle
self.onCouponSelected = onCouponSelected
super.init(nibName: type(of: self).nibName, bundle: nil)
}

Expand Down Expand Up @@ -193,7 +203,7 @@ extension CouponListViewController: UITableViewDelegate {
return
}

onCouponSelected?(coupon)
onCouponSelected(coupon)
}
}

Expand Down Expand Up @@ -288,9 +298,17 @@ private extension CouponListViewController {
let emptyStateViewController = EmptyStateViewController(style: .list)
displayEmptyStateViewController(emptyStateViewController)

if let noResultConfig = noResultConfig {
emptyStateViewController.configure(noResultConfig)
let configuration = EmptyStateViewController.Config.withButton(
message: .init(string: Localization.couponCreationSuggestionMessage),
image: .emptyCouponsImage,
details: Localization.emptyStateDetails,
buttonTitle: emptyStateActionTitle
) { [weak self] _ in
self?.emptyStateAction()
}

emptyStateViewController.configure(configuration)

}


Expand Down Expand Up @@ -383,5 +401,12 @@ private extension CouponListViewController {

static let giveFeedbackAction = NSLocalizedString("Give feedback", comment: "Title of the feedback action button on the coupon list screen")
static let dismissAction = NSLocalizedString("Dismiss", comment: "Title of the dismiss action button on the coupon list screen")

static let couponCreationSuggestionMessage = NSLocalizedString(
"Everyone loves a deal",
comment: "The title on the placeholder overlay when there are no coupons on the coupon list screen and creating a coupon is possible.")
static let emptyStateDetails = NSLocalizedString(
"Boost your business by sending customers special offers and discounts.",
comment: "The details text on the placeholder overlay when there are no coupons on the coupon list screen.")
}
}
Loading

0 comments on commit 519ae01

Please sign in to comment.