Skip to content

Commit

Permalink
Merge branch 'trunk' into feat/14834-replace-local-filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
joshheald authored Jan 16, 2025
2 parents 96fce66 + bf6e6c8 commit f039cb2
Show file tree
Hide file tree
Showing 14 changed files with 934 additions and 135 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
21.5
-----
- [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].

21.4
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,28 +83,32 @@ final class ReceiptEligibilityUseCase: ReceiptEligibilityUseCaseProtocol {
/// WooCommerce 9.5 allows to attach a customer email after payment is made and send email receipt via the API.
/// WooCommerc 9.5 automatically sends failure receipt after the order fails if the customer email is attached to the order.
/// WooPayments 8.6 aligns the app with the web and automatically sets the order as failed when the payment processing fails.
/// Stripe Gateway doesn't automatically set the order to failed therefore the functionality is not supported.
/// WooCommerce Stripe Gateway 9.1.0 aligns the app with the web and automatically sets the order as failed when the payment processing fails.
///
func isEligibleForFailedPaymentEmailReceipts(paymentGatewayID: String, onCompletion: @escaping (Bool) -> Void) {
guard featureFlagService.isFeatureFlagEnabled(.sendReceiptAfterPayment) else {
return onCompletion(false)
}

guard paymentGatewayID == Constants.ReceiptAfterPayment.woocommercePaymentsGatewayID else {
onCompletion(false)
return
}

Task { @MainActor in
async let isWooCommerceSupported = isPluginSupported(Constants.wcPluginName,
minimumVersion: Constants.ReceiptAfterPayment.wcPluginMinimumVersion)
async let isWooPaymentsSupported = isPluginSupported(Constants.wcPayPluginName,
minimumVersion: Constants.ReceiptAfterPayment.wcPayPluginMinimumVersion)
let wooCommerceResult = await isWooCommerceSupported
let wooPaymentsResult = await isWooPaymentsSupported
let isSupported = wooCommerceResult && wooPaymentsResult

onCompletion(isSupported)
async let wooCommerceSupported = isPluginSupported(Constants.wcPluginName,
minimumVersion: Constants.ReceiptAfterPayment.wcPluginMinimumVersion)

async let gatewaySupported: Bool = {
switch paymentGatewayID {
case CardPresentPaymentsPlugin.wcPay.gatewayID:
return await isPluginSupported(CardPresentPaymentsPlugin.wcPay.pluginName,
minimumVersion: Constants.ReceiptAfterPayment.wcPayPluginMinimumVersion)
case CardPresentPaymentsPlugin.stripe.gatewayID:
return await isPluginSupported(CardPresentPaymentsPlugin.stripe.pluginName,
minimumVersion: Constants.ReceiptAfterPayment.stripePluginMinimumVersion)
default:
return false
}
}()

let (isWooCommerceSupported, isGatewaySupported) = await (wooCommerceSupported, gatewaySupported)
onCompletion(isWooCommerceSupported && isGatewaySupported)
}
}
}
Expand Down Expand Up @@ -137,7 +141,6 @@ private extension ReceiptEligibilityUseCase {
private extension ReceiptEligibilityUseCase {
enum Constants {
static let wcPluginName = "WooCommerce"
static let wcPayPluginName = "WooPayments"

enum BackendReceipt {
static let wcPluginMinimumVersion = "8.7.0"
Expand All @@ -147,7 +150,7 @@ private extension ReceiptEligibilityUseCase {
enum ReceiptAfterPayment {
static let wcPluginMinimumVersion = "9.5.0"
static let wcPayPluginMinimumVersion = "8.6.0"
static let woocommercePaymentsGatewayID = "woocommerce-payments"
static let stripePluginMinimumVersion = "9.1.0"
}

enum PointOfSaleReceipts {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ protocol BuiltInCardReaderMerchantEducationPresenting {
func presentMerchantEducation(completion: @escaping () -> Void)
}

final class BuiltInCardReaderMerchantEducationPresenter: BuiltInCardReaderMerchantEducationPresenting {
final class BuiltInCardReaderMerchantEducationPresenter: @preconcurrency BuiltInCardReaderMerchantEducationPresenting {
private weak var rootViewController: ViewControllerPresenting?

init(rootViewController: UIViewController) {
self.rootViewController = rootViewController
}

func presentMerchantEducation(completion: @escaping () -> Void) {
@MainActor func presentMerchantEducation(completion: @escaping () -> Void) {
let viewController = UIHostingController(rootView: TapToPayEducationView(viewModel: .init(completion: { _ in
completion()
})))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ enum TapToPayEducationResult {
case setUpTapToPay
}

@MainActor
final class TapToPayEducationViewModel: ObservableObject {
struct Action {
let title: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ private extension FilterHistoryView {
selected: selectedFilter == filter,
displayMode: .compact,
alignment: .trailing)
.listRowInsets(EdgeInsets())
.onTapGesture {
selectedFilter = filter
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ struct WooShippingEditAddressView: View {

@Environment(\.dismiss) private var dismiss

@State private var isPresentingCountrySelector: Bool = false
@State private var isPresentingStateSelector: Bool = false

var body: some View {
ScrollView {
VStack(spacing: Constants.verticalSpacing) {
AddressTextField(field: .name, text: $viewModel.name, focused: $focusedField)
AddressTextField(field: .name, text: $viewModel.name, required: viewModel.isRequired(.name), focused: $focusedField)
if viewModel.showCompanyField {
AddressTextField(field: .company, text: $viewModel.company, focused: $focusedField)
AddressTextField(field: .company, text: $viewModel.company, required: viewModel.isRequired(.company), focused: $focusedField)
} else {
Button {
withAnimation {
Expand All @@ -34,25 +37,31 @@ struct WooShippingEditAddressView: View {
.font(.subheadline)
.bold()
}
AddressSelection(field: .country, selected: viewModel.country) {
// TODO: Handle country selection
AddressSelection(field: .country, selected: viewModel.selectedCountry?.name ?? "", required: viewModel.isRequired(.country)) {
isPresentingCountrySelector = true
}
.padding(.top, Constants.extraPadding)
AddressTextField(field: .address, text: $viewModel.address, focused: $focusedField)
AddressTextField(field: .city, text: $viewModel.city, focused: $focusedField)
AddressTextField(field: .address, text: $viewModel.address, required: viewModel.isRequired(.address), focused: $focusedField)
AddressTextField(field: .city, text: $viewModel.city, required: viewModel.isRequired(.city), focused: $focusedField)
AdaptiveStack(horizontalAlignment: .leading, verticalAlignment: .top, spacing: Constants.innerSpacing) {
AddressSelection(field: .state, selected: viewModel.state) {
// TODO: Handle state selection
if viewModel.statesOfSelectedCountry.isNotEmpty {
AddressSelection(field: .state, selected: viewModel.selectedState?.name ?? " ", required: viewModel.isRequired(.state)) {
isPresentingStateSelector = true
}
} else {
AddressTextField(field: .state, text: $viewModel.state, required: viewModel.isRequired(.state), focused: $focusedField)
}
AddressTextField(field: .postalCode, text: $viewModel.postalCode, focused: $focusedField)
AddressTextField(field: .postalCode, text: $viewModel.postalCode, required: viewModel.isRequired(.postalCode), focused: $focusedField)
}
.padding(.bottom, Constants.extraPadding)
AddressTextField(field: .email, text: $viewModel.email, focused: $focusedField)
AddressTextField(field: .phone, text: $viewModel.phone, focused: $focusedField)
AddressTextField(field: .email, text: $viewModel.email, required: viewModel.isRequired(.email), focused: $focusedField)
AddressTextField(field: .phone, text: $viewModel.phone, required: viewModel.isRequired(.phone), focused: $focusedField)
.padding(.bottom, Constants.extraPadding)
Toggle(Localization.defaultAddress, isOn: $viewModel.saveAsDefault)
.font(.subheadline)
.tint(Color(.accent))
if viewModel.showSaveAsDefault {
Toggle(Localization.defaultAddress, isOn: $viewModel.isDefaultAddress)
.font(.subheadline)
.tint(Color(.accent))
}
}
.padding()
.toolbar {
Expand Down Expand Up @@ -83,6 +92,38 @@ struct WooShippingEditAddressView: View {
}
}
}
.sheet(isPresented: $isPresentingCountrySelector) {
NavigationStack {
FilterListSelector(viewModel: viewModel.countrySelectorVM)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button {
isPresentingCountrySelector = false
} label: {
Text(Localization.done)
.bold()
}
}
}
}
}
.sheet(isPresented: $isPresentingStateSelector) {
NavigationStack {
FilterListSelector(viewModel: viewModel.stateSelectorVM)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button {
isPresentingStateSelector = false
} label: {
Text(Localization.done)
.bold()
}
}
}
}
}
}
.safeAreaInset(edge: .bottom) {
VStack(spacing: .zero) {
Expand Down Expand Up @@ -117,21 +158,24 @@ struct WooShippingEditAddressView: View {
/// The text to display in the text field.
@Binding var text: String

/// Whether the field is required.
let required: Bool

/// The focused state of the field.
@FocusState.Binding var focused: AddressField?

var body: some View {
VStack(spacing: Constants.innerSpacing) {
HStack(spacing: Constants.requiredLabelSpacing) {
Text(field.title)
if field.required {
if required {
Text("*")
}
Spacer()
}
.font(.subheadline)
.foregroundStyle(Color(.text))
TextField(field.title, text: $text, prompt: Text(field.required ? "" : Localization.optional))
TextField(field.title, text: $text, prompt: Text(required ? "" : Localization.optional))
.focused($focused, equals: field)
.padding()
.roundedBorder(cornerRadius: Constants.cornerRadius,
Expand All @@ -148,14 +192,17 @@ struct WooShippingEditAddressView: View {
/// The text to display for the selection.
let selected: String

/// Whether the field is required.
let required: Bool

/// The action to perform when the button is tapped.
var action: () -> Void

var body: some View {
VStack(spacing: Constants.innerSpacing) {
HStack(spacing: Constants.requiredLabelSpacing) {
Text(field.title)
if field.required {
if required {
Text("*")
}
Spacer()
Expand All @@ -182,7 +229,7 @@ struct WooShippingEditAddressView: View {
}
}

private extension WooShippingEditAddressView {
extension WooShippingEditAddressView {
enum AddressField: CaseIterable {
case name
case company
Expand All @@ -207,19 +254,10 @@ private extension WooShippingEditAddressView {
case .phone: return Localization.phone
}
}

var required: Bool {
switch self {
case .name, .country, .address, .city, .state, .postalCode, .email, .phone:
return true
case .company:
return false
}
}
}

/// Navigates to the next address field in the form.
func focusNextField() {
private func focusNextField() {
switch focusedField {
case .name:
focusedField = viewModel.showCompanyField ? .company : .address
Expand All @@ -241,7 +279,7 @@ private extension WooShippingEditAddressView {
}

/// Navigates to the previous address field in the form.
func focusPreviousField() {
private func focusPreviousField() {
switch focusedField {
case .name:
focusedField = nil
Expand All @@ -263,7 +301,7 @@ private extension WooShippingEditAddressView {
}

/// Dismisses the keyboard.
func dismissKeyboard() {
private func dismissKeyboard() {
focusedField = nil
}
}
Expand Down Expand Up @@ -374,7 +412,8 @@ private extension WooShippingEditAddressView {
}

#Preview("Without Company") {
WooShippingEditAddressView(viewModel: .init(id: UUID().uuidString,
WooShippingEditAddressView(viewModel: .init(type: .origin,
id: UUID().uuidString,
name: "HEADQUARTERS",
company: "",
country: "UNITED STATES",
Expand All @@ -384,13 +423,15 @@ private extension WooShippingEditAddressView {
postalCode: "12883-1487",
email: "",
phone: "",
saveAsDefault: true,
isDefaultAddress: true,
showCompanyField: false,
isVerified: true))
isVerified: true,
phoneNumberRequired: true))
}

#Preview("With Company") {
WooShippingEditAddressView(viewModel: .init(id: UUID().uuidString,
WooShippingEditAddressView(viewModel: .init(type: .destination,
id: UUID().uuidString,
name: "HEADQUARTERS",
company: "COMPANY",
country: "UNITED STATES",
Expand All @@ -400,7 +441,8 @@ private extension WooShippingEditAddressView {
postalCode: "12883-1487",
email: "",
phone: "",
saveAsDefault: false,
isDefaultAddress: false,
showCompanyField: true,
isVerified: false))
isVerified: false,
phoneNumberRequired: true))
}
Loading

0 comments on commit f039cb2

Please sign in to comment.