Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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 @@ -448,6 +448,13 @@ private extension OrderDetailsDataSource {
switch cell {
case let cell as CustomerInfoTableViewCell where row == .shippingAddress:
configureShippingAddress(cell: cell)
case let cell where row == .shippingAddressMap:
if #available(iOS 17.0, *) {
guard let cell = cell as? HostingConfigurationTableViewCell<OrderDetailsShippingAddressMapView> else {
return assertionFailure("Expected HostingConfigurationTableViewCell<OrderDetailsShippingAddressMapView> for shippingAddressMap row")
}
configureShippingAddressMap(cell: cell)
}
case let cell as CustomerNoteTableViewCell where row == .customerNote:
configureCustomerNote(cell: cell)
case let cell as WooBasicTableViewCell where row == .billingDetail:
Expand Down Expand Up @@ -1064,6 +1071,17 @@ private extension OrderDetailsDataSource {
cell.configureLayout()
}

@available(iOS 17.0, *)
private func configureShippingAddressMap(cell: HostingConfigurationTableViewCell<OrderDetailsShippingAddressMapView>) {
let viewModel = OrderDetailsShippingAddressMapViewModel(shippingAddress: order.shippingAddress) { [weak self] in
self?.onCellAction?(.openShippingAddressMap, nil)
}

let view = OrderDetailsShippingAddressMapView(viewModel: viewModel)
cell.host(view)
cell.selectionStyle = .none
}

private func configureShippingLine(cell: HostingConfigurationTableViewCell<ShippingLineRowView>, at indexPath: IndexPath) {
guard let shippingLine = shippingLines[safe: indexPath.row] else {
ServiceLocator.crashLogging.logMessage(
Expand Down Expand Up @@ -1550,8 +1568,11 @@ extension OrderDetailsDataSource {
}.allSatisfy { $0.virtual == true }


if order.shippingAddress != nil && orderContainsOnlyVirtualProducts == false {
if let shippingAddress = order.shippingAddress, orderContainsOnlyVirtualProducts == false {
rows.append(.shippingAddress)
if shippingAddress.formattedPostalAddress != nil, #available(iOS 17.0, *) {
rows.append(.shippingAddressMap)
}
}

/// Billing Address
Expand Down Expand Up @@ -2008,6 +2029,7 @@ extension OrderDetailsDataSource {
case issueRefundButton
case customerNote
case shippingAddress
case shippingAddressMap
case billingDetail
case payment
case customerPaid
Expand Down Expand Up @@ -2062,6 +2084,12 @@ extension OrderDetailsDataSource {
return CustomerNoteTableViewCell.reuseIdentifier
case .shippingAddress:
return CustomerInfoTableViewCell.reuseIdentifier
case .shippingAddressMap:
if #available(iOS 17.0, *) {
return HostingConfigurationTableViewCell<OrderDetailsShippingAddressMapView>.reuseIdentifier
} else {
return UITableViewCell.reuseIdentifier
}
case .billingDetail:
return WooBasicTableViewCell.reuseIdentifier
case .payment:
Expand Down Expand Up @@ -2144,6 +2172,7 @@ extension OrderDetailsDataSource {
case viewAddOns(addOns: [OrderItemProductAddOn])
case editCustomerNote
case editShippingAddress
case openShippingAddressMap
case trashOrder
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import Foundation
import UIKit
import NetworkingCore

/// Helper class to handle opening addresses in Maps apps.
final class OrderDetailsMapLauncher {
static func openAddress(_ address: Address, from viewController: UIViewController) {
openWithCustomURLSchemes(address: address, from: viewController)
}
}

private extension OrderDetailsMapLauncher {
/// Opens address using custom URL schemes with app selection if more than one maps app is available.
static func openWithCustomURLSchemes(address: Address, from viewController: UIViewController) {
let addressString = formatAddressForMaps(address)

let appleURL = createAppleMapsURL(from: addressString)

// Checks availability for each app explicitly.
var availableOptions: [(URL, String)] = []

if let appleURL = appleURL, UIApplication.shared.canOpenURL(appleURL) {
availableOptions.append((appleURL, Localization.appleMaps))
}

// Tries to find an available Google Maps URL.
if let googleURL = findAvailableGoogleMapsURL(for: addressString) {
availableOptions.append((googleURL, Localization.googleMaps))
}

guard !availableOptions.isEmpty else {
// Fallback to web-based maps if no apps are available.
if let webURL = createWebMapsURL(from: addressString) {
UIApplication.shared.open(webURL)
}
return
}

if availableOptions.count == 1 {
UIApplication.shared.open(availableOptions[0].0)
} else {
showActionSheet(options: availableOptions, from: viewController)
}
}

static func formatAddressForMaps(_ address: Address) -> String {
return [
address.address1,
address.address2,
address.city,
address.state,
address.postcode,
address.country
].compactMap { $0?.isEmpty == false ? $0 : nil }.joined(separator: ", ")
}

static func createAppleMapsURL(from addressString: String) -> URL? {
guard let encodedAddress = addressString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
return nil
}
return URL(string: "http://maps.apple.com/?q=\(encodedAddress)")
}

static func findAvailableGoogleMapsURL(for addressString: String) -> URL? {
guard let encodedAddress = addressString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
return nil
}

// Try different Google Maps URL schemes in order of preference
let googleMapsSchemes = [
"googlemaps://?q=\(encodedAddress)", // Modern Google Maps
"comgooglemaps://?q=\(encodedAddress)", // Legacy Google Maps
"gmap://?q=\(encodedAddress)" // Alternative scheme
]

for schemeString in googleMapsSchemes {
if let url = URL(string: schemeString), UIApplication.shared.canOpenURL(url) {
return url
}
}

return nil
}

static func createWebMapsURL(from addressString: String) -> URL? {
guard let encodedAddress = addressString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
return nil
}
return URL(string: "https://maps.google.com/maps?q=\(encodedAddress)")
}

static func showActionSheet(options: [(URL, String)], from viewController: UIViewController) {
let alertController = UIAlertController(
title: NSLocalizedString("Open Address", comment: "Title for the action sheet to open address in maps"),
message: nil,
preferredStyle: .actionSheet
)

for (url, name) in options {
let action = UIAlertAction(title: name, style: .default) { _ in
UIApplication.shared.open(url)
}
alertController.addAction(action)
}

let cancelAction = UIAlertAction(
title: NSLocalizedString("Cancel", comment: "Cancel action for opening address in maps"),
style: .cancel
)
alertController.addAction(cancelAction)

// For iPad support
if let popover = alertController.popoverPresentationController {
popover.sourceView = viewController.view
popover.sourceRect = CGRect(x: viewController.view.bounds.midX, y: viewController.view.bounds.midY, width: 0, height: 0)
popover.permittedArrowDirections = []
}

viewController.present(alertController, animated: true)
}
}

private enum Localization {
static let appleMaps = NSLocalizedString(
"orderDetails.mapLauncher.appleMaps",
value: "Apple Maps",
comment: "Name of Apple Maps app in action sheet"
)
static let googleMaps = NSLocalizedString(
"orderDetails.mapLauncher.googleMaps",
value: "Google Maps",
comment: "Name of Google Maps app in action sheet"
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import SwiftUI
import MapKit

@available(iOS 17.0, *)
struct OrderDetailsShippingAddressMapView: View {
let viewModel: OrderDetailsShippingAddressMapViewModel

var body: some View {
VStack(spacing: 0) {
switch viewModel.mapState {
case let .loaded(coordinate, cameraPosition):
Map(position: .constant(cameraPosition)) {
Annotation("", coordinate: coordinate) {
Image(systemName: "mappin.circle.fill")
.foregroundColor(.red)
.font(.title)
.background(Color.white.clipShape(Circle()))
}
}
.mapStyle(.standard)
.mapControlVisibility(.hidden)
.disabled(true) // Disable user interaction (scrolling, zooming)
.clipShape(RoundedRectangle(cornerRadius: Layout.cornerRadius))
.contentShape(Rectangle())
.onTapGesture {
viewModel.onMapTapped?()
}
case .loading:
RoundedRectangle(cornerRadius: Layout.cornerRadius)
.fill(Color.gray.opacity(0.3))
.overlay(
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
)
case .failed, .none:
RoundedRectangle(cornerRadius: Layout.cornerRadius)
.fill(Color.gray.opacity(0.2))
.overlay(
VStack(spacing: 8) {
Image(systemName: "map")
.foregroundColor(.gray)
.font(.title2)
Text(Localization.tapToOpenInMaps)
.font(.caption2)
.foregroundColor(.secondary)
}
)
.contentShape(Rectangle())
.onTapGesture {
viewModel.onMapTapped?()
}
}
}
.frame(height: viewModel.mapHeight)
.renderedIf(viewModel.isValidAddress)
}
}

@available(iOS 17.0, *)
private extension OrderDetailsShippingAddressMapView {
enum Layout {
static let cornerRadius: CGFloat = 8
}

enum Localization {
static let tapToOpenInMaps = NSLocalizedString(
"orderDetails.shippingAddress.map.tapToOpen",
value: "Tap to open in Maps",
comment: "Text shown to indicate users can tap the map to open the address in Maps app"
)
}
}

#if DEBUG

import struct Yosemite.Address

@available(iOS 17.0, *)
#Preview {
let sampleAddress = Address(
firstName: "",
lastName: "",
company: "",
address1: "60 29th Street #343",
address2: "Suite 100",
city: "San Francisco",
state: "CA",
postcode: "94102",
country: "US",
phone: "+1-555-0123",
email: "[email protected]"
)
let viewModel = OrderDetailsShippingAddressMapViewModel(shippingAddress: sampleAddress)
return OrderDetailsShippingAddressMapView(viewModel: viewModel)
.padding()
}

@available(iOS 17.0, *)
#Preview("Invalid address") {
let sampleAddress = Address(
firstName: "",
lastName: "",
company: "",
address1: "",
address2: "",
city: "ZZ",
state: "",
postcode: "",
country: "US",
phone: "+1-555-0123",
email: "[email protected]"
)
let viewModel = OrderDetailsShippingAddressMapViewModel(shippingAddress: sampleAddress)
return OrderDetailsShippingAddressMapView(viewModel: viewModel)
.padding()
}

@available(iOS 17.0, *)
#Preview("No address") {
let viewModel = OrderDetailsShippingAddressMapViewModel(shippingAddress: nil)
return OrderDetailsShippingAddressMapView(viewModel: viewModel)
.padding()
}

#endif
Loading
Loading