Skip to content

Commit

Permalink
Merge branch 'trunk' into task/14910-pos-cash-testing-feedback-ui-upd…
Browse files Browse the repository at this point in the history
…ates
  • Loading branch information
joshheald authored Jan 23, 2025
2 parents 3ead569 + 65a7b39 commit 203bdf7
Show file tree
Hide file tree
Showing 29 changed files with 756 additions and 310 deletions.
2 changes: 1 addition & 1 deletion Experiments/Experiments/DefaultFeatureFlagService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
case .sendReceiptsForPointOfSale:
return true
case .acceptCashForPointOfSale:
return false
return true
case .tapToPayEducation:
return true
case .variableProductsInPointOfSale:
Expand Down
3 changes: 3 additions & 0 deletions Networking/Networking/Remote/OrdersRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ public class OrdersRemote: Remote {
params[Order.CodingKeys.customerNote.rawValue] = order.customerNote
case .customerID:
params[Order.CodingKeys.customerID.rawValue] = order.customerID
case .currency:
params[Order.CodingKeys.currency.rawValue] = order.currency
}
}

Expand Down Expand Up @@ -490,5 +492,6 @@ public extension OrdersRemote {
case couponLines
case customerNote
case customerID
case currency
}
}
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- [*] Blaze: if the AI is unable to provide suggestions in the creation form, the form will now automatically use product information to ensure all fields of the preview are populated. [https://github.com/woocommerce/woocommerce-ios/pull/14868]
- [Internal] [*] Improved handling of the navigation to the Woo Installation screen post Jetpack setup [https://github.com/woocommerce/woocommerce-ios/pull/14837]
- [*] Receipts: Email receipts can now be sent to customers after failed payments using WooCommerce Stripe 9.1.0+. [https://github.com/woocommerce/woocommerce-ios/pull/14864].
- [*] Order Creation: orders are always created using the store's currency, not the user's [https://github.com/woocommerce/woocommerce-ios/pull/14907]
- [*] Dashboard: Updated the drag gesture on the Performance chart to show stats instantly. [https://github.com/woocommerce/woocommerce-ios/pull/14906]
- [Internal] Removed feedback survey for Store Setup feature [https://github.com/woocommerce/woocommerce-ios/pull/14888]
- [Internal] Removed feedback survey for shipping label creation [https://github.com/woocommerce/woocommerce-ios/pull/14889]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import struct Yosemite.POSCartItem
import enum Yosemite.OrderAction
import enum Yosemite.OrderUpdateField
import class WooFoundation.CurrencyFormatter
import class WooFoundation.CurrencySettings
import enum WooFoundation.CurrencyCode

protocol PointOfSaleOrderControllerProtocol {
var orderStatePublisher: AnyPublisher<PointOfSaleInternalOrderState, Never> { get }
Expand All @@ -26,11 +28,12 @@ final class PointOfSaleOrderController: PointOfSaleOrderControllerProtocol {
init(orderService: POSOrderServiceProtocol,
receiptService: POSReceiptServiceProtocol,
stores: StoresManager = ServiceLocator.stores,
currencyFormatter: CurrencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings)) {
currencySettings: CurrencySettings = ServiceLocator.currencySettings) {
self.orderService = orderService
self.receiptService = receiptService
self.stores = stores
self.currencyFormatter = currencyFormatter
self.storeCurrency = currencySettings.currencyCode
self.currencyFormatter = CurrencyFormatter(currencySettings: currencySettings)
}

var orderStatePublisher: AnyPublisher<PointOfSaleInternalOrderState, Never> {
Expand All @@ -41,6 +44,7 @@ final class PointOfSaleOrderController: PointOfSaleOrderControllerProtocol {
private let receiptService: POSReceiptServiceProtocol

private let currencyFormatter: CurrencyFormatter
private let storeCurrency: CurrencyCode
private let stores: StoresManager

@Published private var orderState: PointOfSaleInternalOrderState = .idle
Expand All @@ -61,7 +65,9 @@ final class PointOfSaleOrderController: PointOfSaleOrderControllerProtocol {
orderState = .syncing

do {
let syncedOrder = try await orderService.syncOrder(cart: posCartItems, order: order)
let syncedOrder = try await orderService.syncOrder(cart: posCartItems,
order: order,
currency: storeCurrency)
self.order = syncedOrder
orderState = .loaded(totals(for: syncedOrder), syncedOrder)
DDLogInfo("🟢 [POS] Synced order: \(syncedOrder)")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import Foundation

struct PointOfSaleCardPresentPaymentReaderDisconnectedMessageViewModel {
let isPOSCashEnabled = ServiceLocator.featureFlagService.isFeatureFlagEnabled(.acceptCashForPointOfSale)

let title = Localization.title
let instruction = Localization.instruction
let connectReaderButtonTitle = Localization.collectPayment
var instruction: String {
isPOSCashEnabled ? Localization.instruction : Localization.cardOnlyInstruction
}
}

private extension PointOfSaleCardPresentPaymentReaderDisconnectedMessageViewModel {
Expand All @@ -14,6 +18,12 @@ private extension PointOfSaleCardPresentPaymentReaderDisconnectedMessageViewMode
comment: "Error message. Presented to users when card reader is not connected on the Point of Sale Checkout"
)

static let cardOnlyInstruction = NSLocalizedString(
"pointOfSale.cardPresent.readerNotConnected.instruction",
value: "To process this payment, please connect your reader.",
comment: "Instruction to merchants shown on the Point of Sale Checkout, so they can take a card payment."
)

static let instruction = NSLocalizedString(
"pointOfSale.cardPresent.readerNotConnectedOrCash.instruction",
value: "To process this payment, please connect your reader or choose cash.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,12 @@ struct ItemList<HeaderView: View>: View {

@ViewBuilder var footerRows: some View {
switch state {
case .loading:
ForEach(0..<8) { _ in
case .loading(let items):
if items.isEmpty {
ForEach(0..<8) { _ in
GhostItemCardView()
}
} else {
GhostItemCardView()
}
case .inlineError(_, let errorState):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class PointOfSalePreviewOrderController: PointOfSaleOrderControllerProtocol {
.init(cartTotal: "$10.50",
orderTotal: "$12.00",
taxTotal: "$1.50"),
OrderFactory.emptyNewOrder
OrderFactory.newOrder(currency: .USD)
)
).eraseToAnyPublisher()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ final class EditableOrderViewModel: ObservableObject {
/// Indicates whether user has made any changes
///
var hasChanges: Bool {
switch flow {
case .creation:
return orderSynchronizer.order != OrderFactory.emptyNewOrder
case .editing(let initialOrder):
return orderSynchronizer.order != initialOrder
if selectionSyncApproach == .onRecalculateButtonTap {
// In split view, we need to check whether the screen has changes that are not yet synced to the order.
return orderSynchronizer.orderHasBeenChanged || syncRequired
} else {
return orderSynchronizer.orderHasBeenChanged
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ protocol OrderSynchronizer {
///
var order: Order { get }

/// Indicates whether the order has been changed in this OrderSynchronizer
var orderHasBeenChanged: Bool { get }

/// Publisher for the order toe be synced or that is synced.
///
var orderPublisher: Published<Order>.Publisher { get }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ final class RemoteOrderSynchronizer: OrderSynchronizer {
$state
}

@Published private(set) var order: Order = OrderFactory.emptyNewOrder
@Published private(set) var order: Order

var orderPublisher: Published<Order>.Publisher {
$order
Expand Down Expand Up @@ -89,6 +89,12 @@ final class RemoteOrderSynchronizer: OrderSynchronizer {
///
private let debounceDuration: TimeInterval

var orderHasBeenChanged: Bool {
return order != initialOrder
}

private let initialOrder: Order

// MARK: Initializers


Expand All @@ -104,8 +110,13 @@ final class RemoteOrderSynchronizer: OrderSynchronizer {
self.debounceDuration = debounceDuration

if case let .editing(initialOrder) = flow {
order = initialOrder
self.initialOrder = initialOrder
self.order = initialOrder
} else {
let storeCurrency = currencySettings.currencyCode
let newOrder = OrderFactory.newOrder(currency: storeCurrency)
self.initialOrder = newOrder
self.order = newOrder
updateBaseSyncOrderStatus()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,29 +73,57 @@ struct WooShippingCustomsForm: View {
ScrollViewReader { proxy in
ScrollView {
VStack(alignment: .leading, spacing: Constants.defaultVerticalSpacing) {
HStack {
Text(Localization.contentType)
Text(Localization.contentType)
.font(.subheadline)
Spacer()
}

contentTypeSelectionView
.padding(.bottom, Constants.defaultVerticalSpacing)

HStack {
Text(Localization.restrictionType)
.font(.subheadline)
Spacer()
Group {
Text(Localization.contentDetails)
.font(.subheadline)
TextField("", text: $viewModel.contentExplanation)
.padding(Constants.borderPadding)
.roundedBorder(cornerRadius: Constants.borderCornerRadius,
lineColor: viewModel.contentExplanation.isEmpty ?
warningRedColor : Color(.separator),
lineWidth: Constants.borderWidth)
Text(Localization.contentDetailsFootnote)
.footnoteStyle()
Text(Localization.valueRequiredWarning)
.foregroundColor(warningRedColor)
.footnoteStyle()
.renderedIf(viewModel.contentExplanation.isEmpty)
}
.renderedIf(viewModel.contentType == .other)


Text(Localization.restrictionType)
.font(.subheadline)

restrictionTypeSelectionView
.padding(.bottom, Constants.defaultVerticalSpacing)

HStack {
Text(Localization.internationalTransactionNumber)
.font(.subheadline)
Spacer()
Group {
Text(Localization.restrictionTypeDetails)
.font(.subheadline)
TextField("", text: $viewModel.restrictionDetails)
.padding(Constants.borderPadding)
.roundedBorder(cornerRadius: Constants.borderCornerRadius,
lineColor: viewModel.restrictionDetails.isEmpty ?
warningRedColor : Color(.separator),
lineWidth: Constants.borderWidth)
Text(Localization.restrictionTypeFootnote)
.footnoteStyle()
Text(Localization.valueRequiredWarning)
.foregroundColor(warningRedColor)
.footnoteStyle()
.renderedIf(viewModel.restrictionDetails.isEmpty)
}
.renderedIf(viewModel.restrictionType == .other)

Text(Localization.internationalTransactionNumber)
.font(.subheadline)

TextField("", text: $viewModel.internationalTransactionNumber)
.padding(Constants.borderPadding)
Expand Down Expand Up @@ -187,6 +215,18 @@ extension WooShippingCustomsForm {
static let contentType = NSLocalizedString("wooShipping.customs.contentType",
value: "Content Type",
comment: "Title for the Content Type menu in the Shipping Customs Form")
static let contentDetails = NSLocalizedString("wooShipping.customs.contentDetails",
value: "Content Details",
comment: "Title for the Content Details text field in the Shipping Customs Form")
static let contentDetailsFootnote = NSLocalizedString("wooShipping.customs.contentDetailsFootnote",
value: "Please describe what kind of goods this package contains",
comment: "Footnote for the Content Details text field in the Shipping Customs Form")
static let restrictionTypeDetails = NSLocalizedString("wooShipping.customs.restrictionTypeDetails",
value: "Restriction Details",
comment: "Title for the Content Details text field in the Shipping Customs Form")
static let restrictionTypeFootnote = NSLocalizedString("wooShipping.customs.restrictionTypeDetailsFootnote",
value: "Please describe what kind of restrictions this package must have",
comment: "Footnote for the Restriction Type text field in the Shipping Customs Form")
static let restrictionType = NSLocalizedString("wooShipping.customs.restrictionType",
value: "Restriction Type",
comment: "Title for the Restriction Type menu in the Shipping Customs Form")
Expand Down Expand Up @@ -215,6 +255,9 @@ extension WooShippingCustomsForm {
value: "International Transaction Number is required for shipping items " +
"valued over $2,500 per tariff number",
comment: "Customs validation warning for the ITN field")
static let valueRequiredWarning = NSLocalizedString("wooShipping.customs.valueRequiredWarning",
value: "Value required",
comment: "Footnote when a required value is missing")

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ final class WooShippingCustomsFormViewModel: ObservableObject {
@Published var requiredInformationIsEntered: Bool = false
@Published var itemsRequiredInformationIsEntered: Bool = false

@Published var contentExplanation: String = ""
@Published var restrictionDetails: String = ""
@Published var contentType: WooShippingContentType = .merchandise
@Published var restrictionType: WooShippingRestrictionType = .none

Expand Down Expand Up @@ -40,9 +42,9 @@ final class WooShippingCustomsFormViewModel: ObservableObject {
let form = ShippingLabelCustomsForm(packageID: "",
packageName: "",
contentsType: contentType.toFormContentsType(),
contentExplanation: "",
contentExplanation: contentType == .other ? contentExplanation : "",
restrictionType: restrictionType.toFormRestrictionType(),
restrictionComments: "",
restrictionComments: restrictionType == .other ? restrictionDetails : "",
nonDeliveryOption: returnToSenderIfNotDelivered ? .return : .abandon,
itn: isValidITN() ? internationalTransactionNumber : "",
items: itemsViewModels.map {
Expand Down Expand Up @@ -77,23 +79,18 @@ final class WooShippingCustomsFormViewModel: ObservableObject {

private extension WooShippingCustomsFormViewModel {
func listenForRequiredInformation() {
Publishers.CombineLatest3($itemsRequiredInformationIsEntered, $internationalTransactionNumber, $internationalTransactionNumberIsRequired)
.sink { [weak self] itemsRequiredInformationIsEntered, internationalTransactionNumber, internationalTransactionNumberIsRequired in
guard let self = self else { return }

guard itemsRequiredInformationIsEntered else {
self.requiredInformationIsEntered = false
return
}

guard internationalTransactionNumberIsRequired else {
self.requiredInformationIsEntered = true
return
}

self.requiredInformationIsEntered = internationalTransactionNumber.isNotEmpty && self.isValidITN()
}
.store(in: &cancellables)
let firstBatch = Publishers.CombineLatest4($contentType, $contentExplanation, $restrictionType, $restrictionDetails)
let secondBatch = Publishers.CombineLatest3($itemsRequiredInformationIsEntered, $internationalTransactionNumber, $internationalTransactionNumberIsRequired)

firstBatch.combineLatest(secondBatch).sink { firstBatchOutput, secondBatchOutput in
let (contentType, contentExplanation, restrictionType, restrictionDetails) = firstBatchOutput
let (itemsRequiredInfo, internationalTransactionNumber, transactionNumberRequired) = secondBatchOutput

self.requiredInformationIsEntered = (contentType != .other || contentExplanation.isNotEmpty) &&
(restrictionType != .other || restrictionDetails.isNotEmpty) &&
itemsRequiredInfo &&
(!transactionNumberRequired || internationalTransactionNumber.isNotEmpty)
}.store(in: &cancellables)
}

func listenForInternationalTransactionNumberIsRequired() {
Expand Down
Loading

0 comments on commit 203bdf7

Please sign in to comment.