Skip to content
Merged
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
1 change: 1 addition & 0 deletions Modules/Sources/Storage/Model/MIGRATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This file documents changes in the WCiOS Storage data model. Please explain any
- Added `shipments` relationship to `Order` entity.
- @itsmeichigo 2025-07-22
- Added `WooShippingOriginAddress` entity.
- Added attributes `lastOrderCompleted` and `addPaymentMethodURL` to `ShippingLabelAccountSettings` entity.

## Model 123 (Release 22.8.0.0)
- @iamgabrielma 2025-06-30
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ extension ShippingLabelAccountSettings {
@NSManaged public var canManagePayments: Bool
@NSManaged public var isEmailReceiptsEnabled: Bool
@NSManaged public var lastSelectedPackageID: String?
@NSManaged public var lastOrderCompleted: Bool
@NSManaged public var paperSize: String?
@NSManaged public var selectedPaymentMethodID: Int64
@NSManaged public var siteID: Int64
@NSManaged public var storeOwnerDisplayName: String?
@NSManaged public var storeOwnerUsername: String?
@NSManaged public var storeOwnerWpcomEmail: String?
@NSManaged public var storeOwnerWpcomUsername: String?
@NSManaged public var addPaymentMethodURL: String?
@NSManaged public var paymentMethods: Set<ShippingLabelPaymentMethod>?

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -778,9 +778,11 @@
<relationship name="shipment" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="WooShippingShipment" inverseName="shippingLabel" inverseEntity="WooShippingShipment"/>
</entity>
<entity name="ShippingLabelAccountSettings" representedClassName="ShippingLabelAccountSettings" syncable="YES">
<attribute name="addPaymentMethodURL" optional="YES" attributeType="String"/>
<attribute name="canEditSettings" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="canManagePayments" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="isEmailReceiptsEnabled" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="lastOrderCompleted" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastSelectedPackageID" attributeType="String" defaultValueString=""/>
<attribute name="paperSize" attributeType="String"/>
<attribute name="selectedPaymentMethodID" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ extension Storage.ShippingLabelAccountSettings: ReadOnlyConvertible {
isEmailReceiptsEnabled = settings.isEmailReceiptsEnabled
paperSize = settings.paperSize.rawValue
lastSelectedPackageID = settings.lastSelectedPackageID
lastOrderCompleted = settings.lastOrderCompleted
addPaymentMethodURL = settings.addPaymentMethodURL?.absoluteString
}

/// Returns a ReadOnly version of the receiver.
Expand All @@ -26,7 +28,6 @@ extension Storage.ShippingLabelAccountSettings: ReadOnlyConvertible {
let paymentMethodItems = paymentMethods?.map { $0.toReadOnly() } ?? []

/// Since account settings are not persisted for the new shipping label flow,
/// the conversion for the new properties `addPaymentMethodURL` & `lastOrderCompleted` is ignored.
/// This avoids the complication of unnecessary Core Data migration for the new properties.
return ShippingLabelAccountSettings(siteID: siteID,
canManagePayments: canManagePayments,
Expand All @@ -40,7 +41,7 @@ extension Storage.ShippingLabelAccountSettings: ReadOnlyConvertible {
isEmailReceiptsEnabled: isEmailReceiptsEnabled,
paperSize: .init(rawValue: paperSize ?? ""),
lastSelectedPackageID: lastSelectedPackageID ?? "",
lastOrderCompleted: false,
addPaymentMethodURL: nil)
lastOrderCompleted: lastOrderCompleted,
addPaymentMethodURL: URL(string: addPaymentMethodURL ?? ""))
}
}
51 changes: 51 additions & 0 deletions Modules/Tests/StorageTests/CoreData/MigrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3518,6 +3518,57 @@ final class MigrationTests: XCTestCase {
let insertedAddress = try XCTUnwrap(targetContext.firstObject(ofType: WooShippingOriginAddress.self))
XCTAssertEqual(insertedAddress, address)
}

func test_migrating_from_123_to_124_adds_new_attributes_lastOrderCompleted_and_addPaymentMethodURL_to_ShippingLabelAccountSettings() throws {
// Given
let sourceContainer = try startPersistentContainer("Model 123")
let sourceContext = sourceContainer.viewContext

let object = sourceContext.insert(entityName: "ShippingLabelAccountSettings", properties: [
"siteID": 123,
"canEditSettings": false,
"canManagePayments": false,
"isEmailReceiptsEnabled": false,
"lastSelectedPackageID": "",
"paperSize": "",
"selectedPaymentMethodID": 0,
"storeOwnerDisplayName": "",
"storeOwnerUsername": "",
"storeOwnerWpcomEmail": "",
"storeOwnerWpcomUsername": ""
])
try sourceContext.save()

// `lastOrderCompleted` and `addPaymentMethodURL` should not be present in model 122
XCTAssertNil(object.entity.attributesByName["lastOrderCompleted"], "Precondition. Attribute does not exist.")
XCTAssertNil(object.entity.attributesByName["addPaymentMethodURL"], "Precondition. Attribute does not exist.")

// When
let targetContainer = try migrate(sourceContainer, to: "Model 124")

// Then
let targetContext = targetContainer.viewContext
let migratedObject = try XCTUnwrap(targetContext.first(entityName: "ShippingLabelAccountSettings"))

// `lastOrderCompleted` should be present in model 124
XCTAssertNotNil(migratedObject.entity.attributesByName["lastOrderCompleted"])

// `addPaymentMethodURL` value should default as nil in model 124
let value = migratedObject.value(forKey: "addPaymentMethodURL") as? String
XCTAssertNil(value)

// `lastOrderCompleted` must be settable
migratedObject.setValue(true, forKey: "lastOrderCompleted")
try targetContext.save()
let updatedValue = migratedObject.value(forKey: "lastOrderCompleted") as? Bool
XCTAssertEqual(updatedValue, true)

// `addPaymentMethodURL` must be settable
migratedObject.setValue("https://example.com", forKey: "addPaymentMethodURL")
try targetContext.save()
let urlValue = migratedObject.value(forKey: "addPaymentMethodURL") as? String
XCTAssertEqual(urlValue, "https://example.com")
}
}

// MARK: - Persistent Store Setup and Migrations
Expand Down
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [*] Shipping Labels: Validate custom package dimensions [https://github.com/woocommerce/woocommerce-ios/pull/15925]
- [*] Shipping Labels: Show UPS TOS modal in full length for better accessibility. [https://github.com/woocommerce/woocommerce-ios/pull/15926]
- [*] Shipping Labels: Optimize data loading on purchase form [https://github.com/woocommerce/woocommerce-ios/pull/15919]
- [*] Shipping Labels: Cache settings and origin addresses to improve loading experience for purchase form [https://github.com/woocommerce/woocommerce-ios/pull/15935]
- [internal] Optimized assets for app size reduction [https://github.com/woocommerce/woocommerce-ios/pull/15881]

22.8
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,27 @@ final class WooShippingCreateLabelsViewModel: ObservableObject {
return ResultsController<StorageWooShippingShipment>(storageManager: storageManager, matching: predicate, sortedBy: [descriptor])
}()

/// Origin addresses Results Controller.
///
private lazy var originAddressResultsController: ResultsController<StorageWooShippingOriginAddress> = {
let predicate = NSPredicate(format: "siteID = %ld", self.order.siteID)
let descriptor = NSSortDescriptor(keyPath: \StorageWooShippingOriginAddress.id, ascending: true)

return ResultsController<StorageWooShippingOriginAddress>(storageManager: storageManager, matching: predicate, sortedBy: [descriptor])
}()

/// Shipping Label Account Settings ResultsController
///
private lazy var accountSettingsResultsController: ResultsController<StorageShippingLabelAccountSettings> = {
let predicate = NSPredicate(format: "siteID == %lld", order.siteID)
return ResultsController<StorageShippingLabelAccountSettings>(
storageManager: storageManager,
matching: predicate,
fetchLimit: 1,
sortedBy: []
)
}()

/// Initialize the view model with or without an existing shipping label.
init(order: Order,
preselection: WooShippingCreateLabelSelection? = nil,
Expand All @@ -241,6 +262,8 @@ final class WooShippingCreateLabelsViewModel: ObservableObject {
self.storageManager = storageManager
self.analytics = analytics
self.shippingSettingsService = shippingSettingsService
self.weightUnit = shippingSettingsService.weightUnit ?? ""
self.dimensionsUnit = shippingSettingsService.dimensionUnit ?? ""
self.initialNoticeDelay = initialNoticeDelay
self.isOrderCompleted = order.status == .completed

Expand All @@ -267,6 +290,8 @@ final class WooShippingCreateLabelsViewModel: ObservableObject {
observeViewStates()
observePaymentMethod()
configureShipmentResultsController()
configureOriginAddressResultsController()
configureAccountSettingsResultsController()

Task { @MainActor in
await loadRequiredData()
Expand All @@ -291,16 +316,24 @@ final class WooShippingCreateLabelsViewModel: ObservableObject {
func loadRequiredData() async {
state = .loading
await withTaskGroup(of: Void.self) { group in
/// Only load store options synchronously if no settings have been saved in storage yet.
if isMissingStoreSettings {
group.addTask {
await self.loadStoreOptions()
}
} else {
/// load asynchronously to update the local storage and unblock UI
stores.dispatch(WooShippingAction.loadAccountSettings(siteID: order.siteID, completion: { _ in }))
}

if hasUnfulfilledShipments {
/// Only load origin addresses synchronously if no addresses have been saved in storage yet.
if hasUnfulfilledShipments, originAddress.isEmpty {
group.addTask {
await self.loadOriginAddresses()
}
} else if hasUnfulfilledShipments {
/// load asynchronously to update the local storage and unblock UI
stores.dispatch(WooShippingAction.loadOriginAddresses(siteID: order.siteID, completion: { _ in }))
}
}

Expand Down Expand Up @@ -418,12 +451,7 @@ private extension WooShippingCreateLabelsViewModel {
}
weightUnit = settings?.storeOptions.weightUnit ?? shippingSettingsService.weightUnit ?? ""
dimensionsUnit = settings?.storeOptions.dimensionUnit ?? shippingSettingsService.dimensionUnit ?? ""
markOrderComplete = settings?.accountSettings.lastOrderCompleted ?? false

if let accountSettings = settings?.accountSettings {
paymentMethodsViewModel = ShippingLabelPaymentMethodsViewModel(accountSettings: accountSettings)
}
setupPaymentMethod(accountSettings: settings?.accountSettings)
updateAccountSettings(accountSettings: settings?.accountSettings)
}

/// Syncs origin addresses to use for shipping label from remote.
Expand All @@ -442,12 +470,7 @@ private extension WooShippingCreateLabelsViewModel {
}
stores.dispatch(action)
}
selectedOriginAddress = addresses.first(where: \.defaultAddress)
originAddresses = WooShippingOriginAddressListViewModel(addresses: addresses,
selectedAddressID: selectedOriginAddress?.id)
originAddresses.onSelect = { [weak self] selectedAddress in
self?.selectedOriginAddress = selectedAddress
}
updateSelectedOriginAddress(addresses: addresses)
}

/// Loads destination address of the order from remote.
Expand Down Expand Up @@ -673,6 +696,70 @@ private extension WooShippingCreateLabelsViewModel {
DDLogError("⛔️ Unable to fetch shipments: \(error)")
}
}

func configureOriginAddressResultsController() {
let updateSelectedAddress = { [weak self] in
guard let self else { return }
let addresses = originAddressResultsController.fetchedObjects
updateSelectedOriginAddress(addresses: addresses)
}
originAddressResultsController.onDidChangeContent = {
updateSelectedAddress()
}

originAddressResultsController.onDidResetContent = {
updateSelectedAddress()
}

do {
try originAddressResultsController.performFetch()
updateSelectedAddress()
} catch {
DDLogError("⛔️ Unable to fetch origin addresses: \(error)")
}
}

func updateSelectedOriginAddress(addresses: [WooShippingOriginAddress]) {
selectedOriginAddress = addresses.first(where: \.defaultAddress)
originAddresses = WooShippingOriginAddressListViewModel(addresses: addresses,
selectedAddressID: selectedOriginAddress?.id)
originAddresses.onSelect = { [weak self] selectedAddress in
self?.selectedOriginAddress = selectedAddress
}
}

/// Shipping Label Account Settings ResultsController monitoring
///
func configureAccountSettingsResultsController() {
let updateSettings = { [weak self] in
guard let self else { return }
let settings = accountSettingsResultsController.fetchedObjects.first
updateAccountSettings(accountSettings: settings)
}
accountSettingsResultsController.onDidChangeContent = {
updateSettings()
}

accountSettingsResultsController.onDidResetContent = {
updateSettings()
}

do {
try accountSettingsResultsController.performFetch()
updateSettings()
} catch {
DDLogError("⛔️ Unable to fetch woo shipping account settings: \(error)")
}
}

func updateAccountSettings(accountSettings: ShippingLabelAccountSettings?) {
markOrderComplete = accountSettings?.lastOrderCompleted ?? false

if let accountSettings {
paymentMethodsViewModel = ShippingLabelPaymentMethodsViewModel(accountSettings: accountSettings)
}
setupPaymentMethod(accountSettings: accountSettings)
}
}

private extension WooShippingCreateLabelsViewModel {
Expand Down
Loading