fix: route payment pushes to orders and prevent refresh crash

This commit is contained in:
2026-09-08 09:59:51 +08:00
parent 10249776ad
commit c4b0e7798b
10 changed files with 333 additions and 47 deletions
+3 -3
View File
@@ -295,7 +295,7 @@ iOS 通过 APNs/极光收到 `userInfo` 字典。与 Android 相比:
| --- | --- |
| 标题示例 | `付款成功通知` |
| 正文示例 | `您的订单 #260722128006 已支付成功,支付金额 ¥0.01` |
| 客户端点击行为 | Android、iOS 均进入收款记录页 |
| 客户端点击行为 | iOS 进入订单 Tab,恢复默认筛选并刷新;Android 行为以 Android 实现为准 |
`data` 字段:
@@ -410,9 +410,9 @@ iOS 通过 APNs/极光收到 `userInfo` 字典。与 Android 相比:
## 7. 客户端解析约定
1. Android、iOS 的通知点击路由只根据业务字段 `type` 判断,不读取标题、正文、`data`、`route`、`uri`、`action` 或极光内部字段决定页面。
2. `type = 1`:进入收款记录页。
2. iOS `type = 1`、`type = 5`:进入订单 Tab 根页面,清空搜索、日期与状态筛选并刷新列表;不进入订单详情。Android 行为以 Android 实现为准。
3. `type = 6`:进入收款详情页。
4. 其他任意 `type`、缺少 `type`、类型无法解析或 payload 损坏:进入消息中心。
4. iOS `type = 14`:有效 `ai_retouch_batch_id` 进入 AI 修图任务详情,否则进入任务列表。其余未单独处理的 `type`、缺少 `type` 或类型无法解析:进入消息中心。
5. Android 从 `notificationExtras` 或厂商通道包装字段中提取 `type`;iOS 优先读取 payload 顶层 `type`,并兼容常见推送包装层中的 `type`。
6. `data` 仍可按消息类型用于页面内容刷新,但不参与通知点击路由。新增字段应保持向后兼容,客户端应忽略当前版本不认识的额外字段。
7. 金额继续使用字符串;时间戳建议统一为 Unix 秒级整数,并在协议中固定单位。
+3 -3
View File
@@ -2,7 +2,7 @@ import Foundation
/// 推送点击后可到达的业务页面。
enum PushDestination: Sendable, Equatable {
case paymentRecord
case orders
case paymentDetails
case messageCenter
case aiRetouchTaskList
@@ -36,8 +36,8 @@ struct PushPayload: Sendable, Equatable {
/// 仅根据后端业务消息类型解析目标页面。
nonisolated var destination: PushDestination {
switch type {
case "1":
return .paymentRecord
case "1", "5":
return .orders
case "6":
return .paymentDetails
case "14":
@@ -8,6 +8,14 @@ import Foundation
/// 店铺管理员押金单列表 ViewModel,对齐 Android `DepositOrderListViewModel`。
final class DepositOrderListViewModel {
private let appStore: AppStore
/// 注入账号上下文,使订单查询与测试使用各自独立的会话。
init(appStore: AppStore = .shared) {
self.appStore = appStore
}
private(set) var searchText = ""
private(set) var filterLabel = "全部"
private(set) var tabIndex = 0
@@ -22,6 +30,7 @@ final class DepositOrderListViewModel {
var onStateChange: (() -> Void)?
var onShowMessage: ((String) -> Void)?
private var listRequestID = UUID()
private var currentPage = 1
private var totalCount = 0
private var searchDebounceTask: Task<Void, Never>?
@@ -34,6 +43,18 @@ final class DepositOrderListViewModel {
return formatter
}()
/// 清除通知进入前的查询条件,由页面随后发起第一页刷新。
func resetFiltersForOrderNotification() {
searchDebounceTask?.cancel()
searchDebounceTask = nil
searchText = ""
filterLabel = "全部"
tabIndex = 0
startTime = nil
endTime = nil
notifyStateChange()
}
func refreshOrderList(api: OrderAPI) async {
currentPage = 1
isRefreshing = true
@@ -102,7 +123,7 @@ final class DepositOrderListViewModel {
onShowMessage?("核销码内容不完整")
return
}
let storeId = AppStore.shared.session.currentStoreId
let storeId = appStore.session.currentStoreId
guard storeId > 0 else {
onShowMessage?("店铺信息异常,请先选择店铺")
return
@@ -125,7 +146,7 @@ final class DepositOrderListViewModel {
refundReason: String,
api: OrderAPI
) async {
let storeId = AppStore.shared.session.currentStoreId
let storeId = appStore.session.currentStoreId
guard storeId > 0 else {
onShowMessage?("店铺信息异常")
return
@@ -163,10 +184,12 @@ final class DepositOrderListViewModel {
}
private func loadOrderList(refresh: Bool, api: OrderAPI) async {
let requestID = UUID()
listRequestID = requestID
let phone = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
let isPhoneSearch = !phone.isEmpty && Self.isValidPhone(phone)
let storeId = AppStore.shared.session.currentStoreId
let storeId = appStore.session.currentStoreId
if storeId <= 0 {
finishLoading()
orderList = []
@@ -201,6 +224,7 @@ final class DepositOrderListViewModel {
startTime: start,
endTime: end
)
guard listRequestID == requestID else { return }
totalCount = response.total
if refresh || currentPage == 1 {
orderList = response.list
@@ -213,6 +237,7 @@ final class DepositOrderListViewModel {
} catch is CancellationError {
return
} catch {
guard listRequestID == requestID else { return }
onShowMessage?(error.localizedDescription)
}
finishLoading()
@@ -83,6 +83,14 @@ struct OrderSourceSelection: Equatable {
/// 摄影师/景区管理员订单列表 ViewModel,对齐 Android `OrderListViewModel`。
final class OrderListViewModel {
private let appStore: AppStore
/// 注入账号上下文,使订单查询与测试使用各自独立的会话。
init(appStore: AppStore = .shared) {
self.appStore = appStore
}
private(set) var orderList: [OrderEntity] = []
private(set) var searchText = ""
private(set) var filterLabel = "全部"
@@ -110,6 +118,7 @@ final class OrderListViewModel {
var onShowMessage: ((String) -> Void)?
var onNavigateBindAcquirer: ((Int) -> Void)?
private var listRequestID = UUID()
private var currentPage = 1
private var totalCount = 0
@@ -122,7 +131,7 @@ final class OrderListViewModel {
}()
var isScenicAdminReadOnly: Bool {
AppStore.shared.session.currentAppRole == .scenicAdmin
appStore.session.currentAppRole == .scenicAdmin
}
var listAPIPath: String {
@@ -131,6 +140,17 @@ final class OrderListViewModel {
: "/api/yf-handset-app/photog/order/listv2"
}
/// 清除通知进入前的查询条件,由页面随后发起第一页刷新。
func resetFiltersForOrderNotification() {
searchText = ""
filterLabel = "全部"
tabIndex = 0
rTabIndex = 0
startTime = nil
endTime = nil
notifyStateChange()
}
func refreshOrderList(api: OrderAPI) async {
currentPage = 1
isRefreshing = true
@@ -359,7 +379,7 @@ final class OrderListViewModel {
isLoadingOrderSourcePeople = false
notifyStateChange()
}
let storeId = AppStore.shared.session.currentStoreId
let storeId = appStore.session.currentStoreId
let storeIdParam = storeId > 0 ? String(storeId) : nil
do {
let response = try await api.cooperativeSalers(storeId: storeIdParam)
@@ -402,7 +422,9 @@ final class OrderListViewModel {
}
private func loadOrderList(api: OrderAPI, append: Bool) async {
let scenicId = AppStore.shared.session.currentScenicId
let requestID = UUID()
listRequestID = requestID
let scenicId = appStore.session.currentScenicId
guard scenicId > 0 else {
finishLoading()
onShowMessage?("当前景区信息缺失")
@@ -436,6 +458,7 @@ final class OrderListViewModel {
isRefined: rTabIndex
)
}
guard listRequestID == requestID else { return }
totalCount = response.total
if append {
orderList.append(contentsOf: response.list)
@@ -447,6 +470,7 @@ final class OrderListViewModel {
} catch is CancellationError {
return
} catch {
guard listRequestID == requestID else { return }
onShowMessage?(error.localizedDescription)
}
finishLoading()
@@ -479,7 +503,7 @@ final class OrderListViewModel {
var json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return nil
}
json["scenic_id"] = String(AppStore.shared.session.currentScenicId)
json["scenic_id"] = String(appStore.session.currentScenicId)
guard let updated = try? JSONSerialization.data(withJSONObject: json),
let string = String(data: updated, encoding: .utf8) else {
return nil
@@ -67,16 +67,20 @@ final class MainTabBarController: UITabBarController {
/// 响应推送点击,切换到目标 Tab 并打开对应业务页面。
func openPushDestination(_ destination: PushDestination) {
let targetTab = AppTab.home
loadViewIfNeeded()
let targetTab: AppTab = destination == .orders ? .orders : .home
let wasAlreadyOnOrders = targetTab == .orders && selectedViewController === tabNavigationControllers[.orders]
selectTab(targetTab)
guard let navigationController = tabNavigationControllers[targetTab] else { return }
_ = navigationController.popToRootViewController(animated: false)
let controller: UIViewController?
switch destination {
case .paymentRecord:
controller = PaymentCollectionRecordViewController()
case .orders:
(navigationController.viewControllers.first as? OrdersViewController)?.openFromOrderNotification()
if wasAlreadyOnOrders { badgeViewModel.refreshPendingWriteOffCount() }
controller = nil
case .paymentDetails:
controller = PaymentCollectionDetailsViewController()
case .messageCenter:
+118 -27
View File
@@ -8,17 +8,64 @@ import SnapKit
import UIKit
/// 订单 Tab 根页面,按角色分流摄影师/景区管理员/店铺管理员。
final class OrdersViewController: BaseViewController, UITableViewDataSource, UITableViewDelegate {
final class OrdersViewController: BaseViewController, UITableViewDelegate {
private let orderAPI = NetworkServices.shared.orderAPI
private let orderListViewModel = OrderListViewModel()
private let depositViewModel = DepositOrderListViewModel()
private let appStore: AppStore
private let orderAPI: OrderAPI
private let orderListViewModel: OrderListViewModel
private let depositViewModel: DepositOrderListViewModel
/// 表格快照持有完整行数据,避免刷新清空 ViewModel 后仍按旧行号取值。
private struct OrderRow: Hashable {
/// 同一快照内按位置区分行,兼容接口返回重复订单号。
let position: Int
let content: Content
/// 摄影师和店铺订单的不可变展示数据。
enum Content: Equatable {
case photographer(OrderEntity, source: OrderSourceSelection?, readOnly: Bool, showSource: Bool)
case deposit(StoreOrderItem)
}
func hash(into hasher: inout Hasher) {
hasher.combine(position)
switch content {
case .photographer(let order, _, _, _):
hasher.combine(0)
hasher.combine(order.orderNumber)
case .deposit(let order):
hasher.combine(1)
hasher.combine(order.orderNumber)
}
}
}
private var dataSource: UITableViewDiffableDataSource<Int, OrderRow>?
/// 注入订单数据依赖;默认使用应用共享 API 和独立的页面 ViewModel。
init(
orderAPI: OrderAPI? = nil,
orderListViewModel: OrderListViewModel? = nil,
depositViewModel: DepositOrderListViewModel? = nil,
appStore: AppStore = .shared
) {
self.appStore = appStore
self.orderAPI = orderAPI ?? NetworkServices.shared.orderAPI
self.orderListViewModel = orderListViewModel ?? OrderListViewModel(appStore: appStore)
self.depositViewModel = depositViewModel ?? DepositOrderListViewModel(appStore: appStore)
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private let filterBar = OrderFilterBarView()
private let listView = OrderListView()
private var hasInitialized = false
private var isStoreMode: Bool { AppStore.shared.session.currentAppRole == .storeAdmin }
private var isStoreMode: Bool { appStore.session.currentAppRole == .storeAdmin }
private var pendingOrderSourceNumber: String?
private weak var orderSourcePicker: OrderSourcePickerViewController?
private var pendingGiftOrderNumber: String?
@@ -32,10 +79,10 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
override func setupUI() {
view.addSubview(filterBar)
view.addSubview(listView)
listView.tableView.dataSource = self
listView.tableView.delegate = self
listView.tableView.register(OrderCardCell.self, forCellReuseIdentifier: OrderCardCell.reuseIdentifier)
listView.tableView.register(DepositOrderCardCell.self, forCellReuseIdentifier: DepositOrderCardCell.reuseIdentifier)
configureDataSource()
}
override func setupConstraints() {
@@ -133,6 +180,21 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
Task { await initializeList() }
}
/// 从订单通知进入时恢复默认查询,并刷新已有或首次打开的列表。
func openFromOrderNotification() {
loadViewIfNeeded()
hasInitialized = true
if isStoreMode {
depositViewModel.resetFiltersForOrderNotification()
} else {
orderListViewModel.resetFiltersForOrderNotification()
}
applyViewModel()
Task { @MainActor [weak self] in
await self?.initializeList()
}
}
private func initializeList() async {
if isStoreMode {
await depositViewModel.refreshOrderList(api: orderAPI)
@@ -142,6 +204,7 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
}
private func applyViewModel() {
applyOrderSnapshot()
if isStoreMode {
filterBar.configure(
isStoreMode: true,
@@ -191,7 +254,6 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
presentMultiVerifySheet()
}
}
listView.tableView.reloadData()
}
private func dateRangeText(start: Date?, end: Date?) -> String {
@@ -314,31 +376,60 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
// MARK: - UITableView
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
isStoreMode ? depositViewModel.orderList.count : orderListViewModel.orderList.count
private func configureDataSource() {
dataSource = UITableViewDiffableDataSource<Int, OrderRow>(tableView: listView.tableView) { [weak self] tableView, _, row in
guard let self else { return nil }
switch row.content {
case .deposit(let item):
let cell = tableView.dequeueReusableCell(withIdentifier: DepositOrderCardCell.reuseIdentifier) as! DepositOrderCardCell
cell.configure(item: item, actions: self.makeDepositActions(for: item))
return cell
case .photographer(let order, let source, let readOnly, let showSource):
let cell = tableView.dequeueReusableCell(withIdentifier: OrderCardCell.reuseIdentifier) as! OrderCardCell
cell.configure(
order: order,
hideWriteActions: readOnly,
actions: self.makeOrderActions(for: order),
showOrderSource: showSource,
sourceSelection: source
)
return cell
}
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
private func applyOrderSnapshot() {
let rows: [OrderRow]
if isStoreMode {
let cell = tableView.dequeueReusableCell(withIdentifier: DepositOrderCardCell.reuseIdentifier, for: indexPath) as! DepositOrderCardCell
let item = depositViewModel.orderList[indexPath.row]
cell.configure(item: item, actions: makeDepositActions(for: item))
listView.notifyLoadMoreIfNeeded(at: indexPath.row, shouldLoad: depositViewModel.shouldLoadMore(at: indexPath.row))
return cell
rows = depositViewModel.orderList.enumerated().map {
OrderRow(position: $0.offset, content: .deposit($0.element))
}
} else {
rows = orderListViewModel.orderList.enumerated().map { index, order in
OrderRow(position: index, content: .photographer(
order,
source: orderListViewModel.orderSourceSelection(for: order),
readOnly: orderListViewModel.isScenicAdminReadOnly,
showSource: orderListViewModel.shouldShowOrderSource(for: order)
))
}
}
var snapshot = NSDiffableDataSourceSnapshot<Int, OrderRow>()
snapshot.appendSections([0])
snapshot.appendItems(rows)
dataSource?.apply(snapshot, animatingDifferences: true)
}
let cell = tableView.dequeueReusableCell(withIdentifier: OrderCardCell.reuseIdentifier, for: indexPath) as! OrderCardCell
let order = orderListViewModel.orderList[indexPath.row]
let source = orderListViewModel.orderSourceSelection(for: order)
cell.configure(
order: order,
hideWriteActions: orderListViewModel.isScenicAdminReadOnly,
actions: makeOrderActions(for: order),
showOrderSource: orderListViewModel.shouldShowOrderSource(for: order),
sourceSelection: source
)
listView.notifyLoadMoreIfNeeded(at: indexPath.row, shouldLoad: orderListViewModel.shouldLoadMore(at: indexPath.row))
return cell
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard let row = dataSource?.itemIdentifier(for: indexPath) else { return }
let shouldLoad: Bool
switch row.content {
case .deposit:
shouldLoad = isStoreMode && depositViewModel.shouldLoadMore(at: row.position)
case .photographer:
shouldLoad = !isStoreMode && orderListViewModel.shouldLoadMore(at: row.position)
}
listView.notifyLoadMoreIfNeeded(at: row.position, shouldLoad: shouldLoad)
}
private func makeOrderActions(for order: OrderEntity) -> OrderCardActions {
@@ -24,6 +24,19 @@ final class DepositOrderListViewModelTests: XCTestCase {
super.tearDown()
}
func testOrderNotificationResetsFilters() {
let viewModel = DepositOrderListViewModel()
viewModel.onSearchTextChange("123", api: OrderAPI(client: APIClient(environment: .testing, session: MockURLSession(results: []))))
viewModel.onFilterChange(tab: 18)
viewModel.onDateRangeSelected(start: Date(), end: Date())
viewModel.resetFiltersForOrderNotification()
XCTAssertEqual(viewModel.searchText, "")
XCTAssertEqual(viewModel.tabIndex, 0)
XCTAssertEqual(viewModel.filterLabel, "全部")
XCTAssertNil(viewModel.startTime)
XCTAssertNil(viewModel.endTime)
}
func testStoreFilterMapsPaidStatusToAPI20() {
XCTAssertEqual(DepositOrderListViewModel.apiOrderStatus(forTabIndex: 18), 20)
XCTAssertEqual(DepositOrderListViewModel.apiOrderStatus(forTabIndex: 30), 30)
@@ -72,3 +72,88 @@ private extension UIColor {
return UInt(r << 16 | g << 8 | b)
}
}
/// 回归通知刷新时表格仍请求旧行的场景,使用 mock 数据覆盖两种订单页面。
@MainActor
final class OrderNotificationListSnapshotTests: XCTestCase {
private var appStore: AppStore!
private var suiteName: String!
override func setUp() {
super.setUp()
suiteName = "OrderNotificationListSnapshotTests.\(UUID().uuidString)"
appStore = AppStore(defaults: UserDefaults(suiteName: suiteName)!)
appStore.session.configureTestAccount(userId: "push-list-test", accountType: .storeUser, scenicId: 10)
appStore.session.currentStoreId = 20
}
override func tearDown() {
UserDefaults.standard.removePersistentDomain(forName: suiteName)
appStore = nil
super.tearDown()
}
func testPhotographerRowsRemainReadableWhileNotificationRefreshClearsModel() async throws {
try await verifyRefreshUsesSnapshot(storeMode: false)
}
func testDepositRowsRemainReadableWhileNotificationRefreshClearsModel() async throws {
try await verifyRefreshUsesSnapshot(storeMode: true)
}
private func verifyRefreshUsesSnapshot(storeMode: Bool) async throws {
appStore.session.roleCode = storeMode ? AppRoleCode.storeAdmin.rawValue : AppRoleCode.photographer.rawValue
let initial = Data(#"{"code":100000,"msg":"success","data":{"list":[{"order_number":"push-order-a","order_status":30},{"order_number":"push-order-b","order_status":30}],"total":2}}"#.utf8)
let empty = Data(#"{"code":100000,"msg":"success","data":{"list":[],"total":0}}"#.utf8)
let session = MockURLSession(responses: [initial, empty, initial])
let api = OrderAPI(client: APIClient(environment: .testing, session: session))
let orders = OrderListViewModel(appStore: appStore)
let deposits = DepositOrderListViewModel(appStore: appStore)
let controller = OrdersViewController(orderAPI: api, orderListViewModel: orders, depositViewModel: deposits, appStore: appStore)
controller.openFromOrderNotification()
let list = try XCTUnwrap(controller.view.subviews.first { $0 is OrderListView } as? OrderListView)
let table = list.tableView
await waitForRows(2, in: table)
let source = try XCTUnwrap(table.dataSource)
// 保持表格的旧快照,精确模拟模型已清空、主线程 UI 回调尚未执行的窗口。
let notify = storeMode ? deposits.onStateChange : orders.onStateChange
if storeMode {
deposits.onStateChange = nil
deposits.resetFiltersForOrderNotification()
await deposits.refreshOrderList(api: api)
XCTAssertTrue(deposits.orderList.isEmpty)
} else {
orders.onStateChange = nil
orders.resetFiltersForOrderNotification()
await orders.refreshOrderList(api: api)
XCTAssertTrue(orders.orderList.isEmpty)
}
XCTAssertEqual(source.tableView(table, numberOfRowsInSection: 0), 2)
for index in 0..<2 {
let cell = source.tableView(table, cellForRowAt: IndexPath(row: index, section: 0))
if storeMode {
XCTAssertTrue(cell is DepositOrderCardCell)
} else {
XCTAssertTrue(cell is OrderCardCell)
}
}
if storeMode { deposits.onStateChange = notify } else { orders.onStateChange = notify }
notify?()
await waitForRows(0, in: table)
// 已打开的订单页再次点击通知,仍能加载完整列表。
controller.openFromOrderNotification()
await waitForRows(2, in: table)
XCTAssertEqual(session.requests.count, 3)
}
private func waitForRows(_ count: Int, in table: UITableView) async {
let applied = expectation(for: NSPredicate { _, _ in
MainActor.assumeIsolated {
table.numberOfRows(inSection: 0) == count
}
}, evaluatedWith: nil)
await fulfillment(of: [applied], timeout: 3)
}
}
@@ -20,6 +20,20 @@ final class OrderListViewModelTests: XCTestCase {
super.tearDown()
}
func testOrderNotificationResetsFilters() {
let viewModel = OrderListViewModel()
viewModel.onSearchTextChange("13800138000")
viewModel.onFilterChange(orderStatus: 18, isRefined: 1)
viewModel.onDateRangeSelected(start: Date(), end: Date())
viewModel.resetFiltersForOrderNotification()
XCTAssertEqual(viewModel.searchText, "")
XCTAssertEqual(viewModel.tabIndex, 0)
XCTAssertEqual(viewModel.filterLabel, "全部")
XCTAssertNil(viewModel.startTime)
XCTAssertNil(viewModel.endTime)
XCTAssertEqual(viewModel.rTabIndex, 0)
}
func testOrderStatusQueryMapping() {
XCTAssertNil(OrderListQueryBuilder.orderStatusQuery(tabIndex: 0))
XCTAssertEqual(OrderListQueryBuilder.orderStatusQuery(tabIndex: 18), "18")
+34 -4
View File
@@ -27,10 +27,10 @@ final class PushNotificationTests: XCTestCase {
super.tearDown()
}
func testPayloadRoutesPaymentTypesToTheirPages() {
func testPayloadRoutesOrderAndScanTypesToTheirPages() {
XCTAssertEqual(
PushPayload(userInfo: ["type": 1, "route": "message_center"]).destination,
.paymentRecord
.orders
)
XCTAssertEqual(
PushPayload(userInfo: ["type": "6", "action": "task_management"]).destination,
@@ -38,6 +38,36 @@ final class PushNotificationTests: XCTestCase {
)
}
func testOrderTypesSupportNumbersStringsAndVendorWrappers() {
for type in [1, 5] {
XCTAssertEqual(PushPayload(userInfo: ["type": type]).destination, .orders)
XCTAssertEqual(PushPayload(userInfo: ["type": "\(type)"]).destination, .orders)
XCTAssertEqual(PushPayload(userInfo: ["extras": "{\"type\":\(type)}"]).destination, .orders)
XCTAssertEqual(PushPayload(values: ["type": "\(type)"]).destination, .orders)
}
}
func testOrderNotificationReturnsToOrdersRootAndDeduplicatesPendingRoute() {
let coordinator = PushRouteCoordinator(appStore: appStore)
let window = UIWindow(frame: UIScreen.main.bounds)
coordinator.attach(window: window)
coordinator.handle(destination: .orders, requestIdentifier: "order-request")
authenticate(userID: "100")
let mainTab = MainTabBarController()
mainTab.loadViewIfNeeded()
mainTab.selectTab(.orders)
let ordersNavigation = mainTab.selectedViewController as? TabNavigationController
ordersNavigation?.pushViewController(UIViewController(), animated: false)
mainTab.selectTab(.home)
window.rootViewController = mainTab
coordinator.routePendingIfPossible()
XCTAssertTrue(mainTab.selectedViewController === ordersNavigation)
XCTAssertEqual(ordersNavigation?.viewControllers.count, 1)
XCTAssertTrue(ordersNavigation?.topViewController is OrdersViewController)
coordinator.handle(destination: .messageCenter, requestIdentifier: "order-request")
XCTAssertTrue(mainTab.selectedViewController === ordersNavigation)
}
func testPayloadIgnoresNonTypeRoutingFieldsAndOtherTypesFallBackToMessageCenter() {
let conflictingFields: [AnyHashable: Any] = [
"type": 2,
@@ -46,7 +76,7 @@ final class PushNotificationTests: XCTestCase {
"action": "收款",
]
[0, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 100, 999].forEach { type in
[0, 2, 3, 4, 7, 8, 9, 10, 11, 12, 100, 999].forEach { type in
XCTAssertEqual(PushPayload(userInfo: ["type": type]).destination, .messageCenter)
}
XCTAssertEqual(PushPayload(userInfo: conflictingFields).destination, .messageCenter)
@@ -220,7 +250,7 @@ final class PushNotificationTests: XCTestCase {
payload: PushPayload(userInfo: ["type": 1, "route": "task_management"]),
requestIdentifier: "request-1"
)
XCTAssertEqual(router.destinations, [.paymentRecord])
XCTAssertEqual(router.destinations, [.orders])
}
func testRemoteNotificationNotifiesUnreadMessageStateChange() async {