Compare commits
6 Commits
main
...
7ccbc5568d
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ccbc5568d | |||
| 787a166263 | |||
| 74f7370aab | |||
| 1b2637f0ee | |||
| caeeb9a1cf | |||
| e7f1d777dd |
3
Podfile
3
Podfile
@ -8,6 +8,9 @@ target 'suixinkan' do
|
|||||||
pod 'UMCommon'
|
pod 'UMCommon'
|
||||||
pod 'UMDevice'
|
pod 'UMDevice'
|
||||||
pod 'UMAPM'
|
pod 'UMAPM'
|
||||||
|
# 极光推送:固定无 IDFA 的 JCore,避免引入 ATT 授权。
|
||||||
|
pod 'JCore', '5.4.2-noidfa'
|
||||||
|
pod 'JPush', '6.1.0'
|
||||||
pod 'IQKeyboardManagerSwift'
|
pod 'IQKeyboardManagerSwift'
|
||||||
# 微信 Open SDK:仅接入分享能力,不调用支付 API。
|
# 微信 Open SDK:仅接入分享能力,不调用支付 API。
|
||||||
pod 'WechatOpenSDK-XCFramework', '~> 2.0.4'
|
pod 'WechatOpenSDK-XCFramework', '~> 2.0.4'
|
||||||
|
|||||||
11
Podfile.lock
11
Podfile.lock
@ -46,6 +46,9 @@ PODS:
|
|||||||
- IQKeyboardCore
|
- IQKeyboardCore
|
||||||
- IQTextView (1.0.5):
|
- IQTextView (1.0.5):
|
||||||
- IQKeyboardToolbar/Placeholderable
|
- IQKeyboardToolbar/Placeholderable
|
||||||
|
- JCore (5.4.2-noidfa)
|
||||||
|
- JPush (6.1.0):
|
||||||
|
- JCore (> 5.1.1)
|
||||||
- UMAPM (2.0.7):
|
- UMAPM (2.0.7):
|
||||||
- UMCommon
|
- UMCommon
|
||||||
- UMCommon (7.5.11):
|
- UMCommon (7.5.11):
|
||||||
@ -58,6 +61,8 @@ DEPENDENCIES:
|
|||||||
- AMapLocation-NO-IDFA
|
- AMapLocation-NO-IDFA
|
||||||
- AMapSearch-NO-IDFA
|
- AMapSearch-NO-IDFA
|
||||||
- IQKeyboardManagerSwift
|
- IQKeyboardManagerSwift
|
||||||
|
- JCore (= 5.4.2-noidfa)
|
||||||
|
- JPush (= 6.1.0)
|
||||||
- UMAPM
|
- UMAPM
|
||||||
- UMCommon
|
- UMCommon
|
||||||
- UMDevice
|
- UMDevice
|
||||||
@ -77,6 +82,8 @@ SPEC REPOS:
|
|||||||
- IQKeyboardToolbarManager
|
- IQKeyboardToolbarManager
|
||||||
- IQTextInputViewNotification
|
- IQTextInputViewNotification
|
||||||
- IQTextView
|
- IQTextView
|
||||||
|
- JCore
|
||||||
|
- JPush
|
||||||
- UMAPM
|
- UMAPM
|
||||||
- UMCommon
|
- UMCommon
|
||||||
- UMDevice
|
- UMDevice
|
||||||
@ -95,11 +102,13 @@ SPEC CHECKSUMS:
|
|||||||
IQKeyboardToolbarManager: 242e858cc74e06e00aa989bea1355b24d4397854
|
IQKeyboardToolbarManager: 242e858cc74e06e00aa989bea1355b24d4397854
|
||||||
IQTextInputViewNotification: 3b9fb27a16e7ee8958cc9092cfb07a1a9e1fd559
|
IQTextInputViewNotification: 3b9fb27a16e7ee8958cc9092cfb07a1a9e1fd559
|
||||||
IQTextView: ae13b4922f22e6f027f62c557d9f4f236b19d5c7
|
IQTextView: ae13b4922f22e6f027f62c557d9f4f236b19d5c7
|
||||||
|
JCore: 696e0ac22f417bb0ac4729fc41bc2941aef14e2b
|
||||||
|
JPush: 891c497687be788b094bde368d51289355ee1112
|
||||||
UMAPM: ba9fbebe8dd2048b251c332ab0afc86e1ba5ea63
|
UMAPM: ba9fbebe8dd2048b251c332ab0afc86e1ba5ea63
|
||||||
UMCommon: d652b3b372a801b36db203f5b546ab3e15676898
|
UMCommon: d652b3b372a801b36db203f5b546ab3e15676898
|
||||||
UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19
|
UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19
|
||||||
WechatOpenSDK-XCFramework: ff342ae616bb86df3d236aca38059dfd4bc4a949
|
WechatOpenSDK-XCFramework: ff342ae616bb86df3d236aca38059dfd4bc4a949
|
||||||
|
|
||||||
PODFILE CHECKSUM: 0fd38f741cbaae39c8e7a70fa233f651d3b34d0f
|
PODFILE CHECKSUM: e7971a58b5d46ffc03f36ef716b922b6a75a6412
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
1
Pods/JCore/README.md
generated
Normal file
1
Pods/JCore/README.md
generated
Normal file
@ -0,0 +1 @@
|
|||||||
|
# jcore-sdk
|
||||||
44
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/Info.plist
generated
Normal file
44
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/Info.plist
generated
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>AvailableLibraries</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>HeadersPath</key>
|
||||||
|
<string>Headers</string>
|
||||||
|
<key>LibraryIdentifier</key>
|
||||||
|
<string>ios-arm64</string>
|
||||||
|
<key>LibraryPath</key>
|
||||||
|
<string>libJCore.a</string>
|
||||||
|
<key>SupportedArchitectures</key>
|
||||||
|
<array>
|
||||||
|
<string>arm64</string>
|
||||||
|
</array>
|
||||||
|
<key>SupportedPlatform</key>
|
||||||
|
<string>ios</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>HeadersPath</key>
|
||||||
|
<string>Headers</string>
|
||||||
|
<key>LibraryIdentifier</key>
|
||||||
|
<string>ios-arm64_x86_64-simulator</string>
|
||||||
|
<key>LibraryPath</key>
|
||||||
|
<string>libJCore.a</string>
|
||||||
|
<key>SupportedArchitectures</key>
|
||||||
|
<array>
|
||||||
|
<string>arm64</string>
|
||||||
|
<string>x86_64</string>
|
||||||
|
</array>
|
||||||
|
<key>SupportedPlatform</key>
|
||||||
|
<string>ios</string>
|
||||||
|
<key>SupportedPlatformVariant</key>
|
||||||
|
<string>simulator</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>XFWK</string>
|
||||||
|
<key>XCFrameworkFormatVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
19
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/Headers/JGCOREAPI.h
generated
Normal file
19
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/Headers/JGCOREAPI.h
generated
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// JGCOREService.h
|
||||||
|
// JCore
|
||||||
|
//
|
||||||
|
// Created by Shuni Huang on 2024/3/19.
|
||||||
|
// Copyright © 2024 jiguang. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface JGCOREAPI : NSObject
|
||||||
|
|
||||||
|
+ (void)ci:(BOOL)enable;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
27
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/Headers/JGInforCollectionAuth.h
generated
Normal file
27
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/Headers/JGInforCollectionAuth.h
generated
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// JGInforCollectionAuth.h
|
||||||
|
// JCore
|
||||||
|
//
|
||||||
|
// Created by 豆瓣 on 2021/10/27.
|
||||||
|
// Copyright © 2021 jiguang. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/// 合规接口,是否授权极光采集一定的设备信息
|
||||||
|
@interface JGInforCollectionAuthItems : NSObject
|
||||||
|
/// 是否授权,默认YES
|
||||||
|
@property(nonatomic,assign)BOOL isAuth;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface JGInforCollectionAuth : NSObject
|
||||||
|
|
||||||
|
/// 设备信息采集授权接口(合规接口)
|
||||||
|
/// 请务必在调用初始化、功能性接口前调用此接口进行合规授权
|
||||||
|
/// @param authBlock auth:YES 则极光认为您同意极光采集一定的设备信息
|
||||||
|
+(void)JCollectionAuth:(void(^_Nullable)(JGInforCollectionAuthItems *authInfo))authBlock;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
64
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/PrivacyInfo.xcprivacy
generated
Normal file
64
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/PrivacyInfo.xcprivacy
generated
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyTrackingDomains</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeCrashData</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypePreciseLocation</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>E174.1</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>CA92.1</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>C617.1</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/libJCore.a
generated
Normal file
BIN
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/libJCore.a
generated
Normal file
Binary file not shown.
19
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator/Headers/JGCOREAPI.h
generated
Normal file
19
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator/Headers/JGCOREAPI.h
generated
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// JGCOREService.h
|
||||||
|
// JCore
|
||||||
|
//
|
||||||
|
// Created by Shuni Huang on 2024/3/19.
|
||||||
|
// Copyright © 2024 jiguang. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface JGCOREAPI : NSObject
|
||||||
|
|
||||||
|
+ (void)ci:(BOOL)enable;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// JGInforCollectionAuth.h
|
||||||
|
// JCore
|
||||||
|
//
|
||||||
|
// Created by 豆瓣 on 2021/10/27.
|
||||||
|
// Copyright © 2021 jiguang. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/// 合规接口,是否授权极光采集一定的设备信息
|
||||||
|
@interface JGInforCollectionAuthItems : NSObject
|
||||||
|
/// 是否授权,默认YES
|
||||||
|
@property(nonatomic,assign)BOOL isAuth;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface JGInforCollectionAuth : NSObject
|
||||||
|
|
||||||
|
/// 设备信息采集授权接口(合规接口)
|
||||||
|
/// 请务必在调用初始化、功能性接口前调用此接口进行合规授权
|
||||||
|
/// @param authBlock auth:YES 则极光认为您同意极光采集一定的设备信息
|
||||||
|
+(void)JCollectionAuth:(void(^_Nullable)(JGInforCollectionAuthItems *authInfo))authBlock;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
64
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator/PrivacyInfo.xcprivacy
generated
Normal file
64
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator/PrivacyInfo.xcprivacy
generated
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyTrackingDomains</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeCrashData</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypePreciseLocation</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>E174.1</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>CA92.1</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>C617.1</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator/libJCore.a
generated
Normal file
BIN
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator/libJCore.a
generated
Normal file
Binary file not shown.
44
Pods/JPush/jpush-ios-6.1.0.xcframework/Info.plist
generated
Normal file
44
Pods/JPush/jpush-ios-6.1.0.xcframework/Info.plist
generated
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>AvailableLibraries</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>HeadersPath</key>
|
||||||
|
<string>Headers</string>
|
||||||
|
<key>LibraryIdentifier</key>
|
||||||
|
<string>ios-arm64_x86_64-simulator</string>
|
||||||
|
<key>LibraryPath</key>
|
||||||
|
<string>libJPush.a</string>
|
||||||
|
<key>SupportedArchitectures</key>
|
||||||
|
<array>
|
||||||
|
<string>arm64</string>
|
||||||
|
<string>x86_64</string>
|
||||||
|
</array>
|
||||||
|
<key>SupportedPlatform</key>
|
||||||
|
<string>ios</string>
|
||||||
|
<key>SupportedPlatformVariant</key>
|
||||||
|
<string>simulator</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>HeadersPath</key>
|
||||||
|
<string>Headers</string>
|
||||||
|
<key>LibraryIdentifier</key>
|
||||||
|
<string>ios-arm64</string>
|
||||||
|
<key>LibraryPath</key>
|
||||||
|
<string>libJPush.a</string>
|
||||||
|
<key>SupportedArchitectures</key>
|
||||||
|
<array>
|
||||||
|
<string>arm64</string>
|
||||||
|
</array>
|
||||||
|
<key>SupportedPlatform</key>
|
||||||
|
<string>ios</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>XFWK</string>
|
||||||
|
<key>XCFrameworkFormatVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
1023
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64/Headers/JPUSHService.h
generated
Normal file
1023
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64/Headers/JPUSHService.h
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64/PrivacyInfo.xcprivacy
generated
Normal file
23
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64/PrivacyInfo.xcprivacy
generated
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyTrackingDomains</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>CA92.1</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64/libJPush.a
generated
Normal file
BIN
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64/libJPush.a
generated
Normal file
Binary file not shown.
1023
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator/Headers/JPUSHService.h
generated
Normal file
1023
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator/Headers/JPUSHService.h
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator/PrivacyInfo.xcprivacy
generated
Normal file
23
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator/PrivacyInfo.xcprivacy
generated
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyTrackingDomains</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>CA92.1</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator/libJPush.a
generated
Normal file
BIN
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator/libJPush.a
generated
Normal file
Binary file not shown.
11
Pods/Manifest.lock
generated
11
Pods/Manifest.lock
generated
@ -46,6 +46,9 @@ PODS:
|
|||||||
- IQKeyboardCore
|
- IQKeyboardCore
|
||||||
- IQTextView (1.0.5):
|
- IQTextView (1.0.5):
|
||||||
- IQKeyboardToolbar/Placeholderable
|
- IQKeyboardToolbar/Placeholderable
|
||||||
|
- JCore (5.4.2-noidfa)
|
||||||
|
- JPush (6.1.0):
|
||||||
|
- JCore (> 5.1.1)
|
||||||
- UMAPM (2.0.7):
|
- UMAPM (2.0.7):
|
||||||
- UMCommon
|
- UMCommon
|
||||||
- UMCommon (7.5.11):
|
- UMCommon (7.5.11):
|
||||||
@ -58,6 +61,8 @@ DEPENDENCIES:
|
|||||||
- AMapLocation-NO-IDFA
|
- AMapLocation-NO-IDFA
|
||||||
- AMapSearch-NO-IDFA
|
- AMapSearch-NO-IDFA
|
||||||
- IQKeyboardManagerSwift
|
- IQKeyboardManagerSwift
|
||||||
|
- JCore (= 5.4.2-noidfa)
|
||||||
|
- JPush (= 6.1.0)
|
||||||
- UMAPM
|
- UMAPM
|
||||||
- UMCommon
|
- UMCommon
|
||||||
- UMDevice
|
- UMDevice
|
||||||
@ -77,6 +82,8 @@ SPEC REPOS:
|
|||||||
- IQKeyboardToolbarManager
|
- IQKeyboardToolbarManager
|
||||||
- IQTextInputViewNotification
|
- IQTextInputViewNotification
|
||||||
- IQTextView
|
- IQTextView
|
||||||
|
- JCore
|
||||||
|
- JPush
|
||||||
- UMAPM
|
- UMAPM
|
||||||
- UMCommon
|
- UMCommon
|
||||||
- UMDevice
|
- UMDevice
|
||||||
@ -95,11 +102,13 @@ SPEC CHECKSUMS:
|
|||||||
IQKeyboardToolbarManager: 242e858cc74e06e00aa989bea1355b24d4397854
|
IQKeyboardToolbarManager: 242e858cc74e06e00aa989bea1355b24d4397854
|
||||||
IQTextInputViewNotification: 3b9fb27a16e7ee8958cc9092cfb07a1a9e1fd559
|
IQTextInputViewNotification: 3b9fb27a16e7ee8958cc9092cfb07a1a9e1fd559
|
||||||
IQTextView: ae13b4922f22e6f027f62c557d9f4f236b19d5c7
|
IQTextView: ae13b4922f22e6f027f62c557d9f4f236b19d5c7
|
||||||
|
JCore: 696e0ac22f417bb0ac4729fc41bc2941aef14e2b
|
||||||
|
JPush: 891c497687be788b094bde368d51289355ee1112
|
||||||
UMAPM: ba9fbebe8dd2048b251c332ab0afc86e1ba5ea63
|
UMAPM: ba9fbebe8dd2048b251c332ab0afc86e1ba5ea63
|
||||||
UMCommon: d652b3b372a801b36db203f5b546ab3e15676898
|
UMCommon: d652b3b372a801b36db203f5b546ab3e15676898
|
||||||
UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19
|
UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19
|
||||||
WechatOpenSDK-XCFramework: ff342ae616bb86df3d236aca38059dfd4bc4a949
|
WechatOpenSDK-XCFramework: ff342ae616bb86df3d236aca38059dfd4bc4a949
|
||||||
|
|
||||||
PODFILE CHECKSUM: 0fd38f741cbaae39c8e7a70fa233f651d3b34d0f
|
PODFILE CHECKSUM: e7971a58b5d46ffc03f36ef716b922b6a75a6412
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
5216
Pods/Pods.xcodeproj/project.pbxproj
generated
5216
Pods/Pods.xcodeproj/project.pbxproj
generated
File diff suppressed because it is too large
Load Diff
2
Pods/Target Support Files/JCore/JCore-xcframeworks-input-files.xcfilelist
generated
Normal file
2
Pods/Target Support Files/JCore/JCore-xcframeworks-input-files.xcfilelist
generated
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
${PODS_ROOT}/Target Support Files/JCore/JCore-xcframeworks.sh
|
||||||
|
${PODS_ROOT}/JCore/jcore-noidfa-ios-5.4.2.xcframework
|
||||||
1
Pods/Target Support Files/JCore/JCore-xcframeworks-output-files.xcfilelist
generated
Normal file
1
Pods/Target Support Files/JCore/JCore-xcframeworks-output-files.xcfilelist
generated
Normal file
@ -0,0 +1 @@
|
|||||||
|
${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/jcore-noidfa-ios-5.4.2.framework
|
||||||
121
Pods/Target Support Files/JCore/JCore-xcframeworks.sh
generated
Executable file
121
Pods/Target Support Files/JCore/JCore-xcframeworks.sh
generated
Executable file
@ -0,0 +1,121 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
set -u
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
function on_error {
|
||||||
|
echo "$(realpath -mq "${0}"):$1: error: Unexpected failure"
|
||||||
|
}
|
||||||
|
trap 'on_error $LINENO' ERR
|
||||||
|
|
||||||
|
|
||||||
|
# This protects against multiple targets copying the same framework dependency at the same time. The solution
|
||||||
|
# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html
|
||||||
|
RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????")
|
||||||
|
|
||||||
|
|
||||||
|
variant_for_slice()
|
||||||
|
{
|
||||||
|
case "$1" in
|
||||||
|
"jcore-noidfa-ios-5.4.2.xcframework/ios-arm64")
|
||||||
|
echo ""
|
||||||
|
;;
|
||||||
|
"jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator")
|
||||||
|
echo "simulator"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
archs_for_slice()
|
||||||
|
{
|
||||||
|
case "$1" in
|
||||||
|
"jcore-noidfa-ios-5.4.2.xcframework/ios-arm64")
|
||||||
|
echo "arm64"
|
||||||
|
;;
|
||||||
|
"jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator")
|
||||||
|
echo "arm64 x86_64"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_dir()
|
||||||
|
{
|
||||||
|
local source="$1"
|
||||||
|
local destination="$2"
|
||||||
|
|
||||||
|
# Use filter instead of exclude so missing patterns don't throw errors.
|
||||||
|
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" \"${source}*\" \"${destination}\""
|
||||||
|
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" "${source}"/* "${destination}"
|
||||||
|
}
|
||||||
|
|
||||||
|
SELECT_SLICE_RETVAL=""
|
||||||
|
|
||||||
|
select_slice() {
|
||||||
|
local xcframework_name="$1"
|
||||||
|
xcframework_name="${xcframework_name##*/}"
|
||||||
|
local paths=("${@:2}")
|
||||||
|
# Locate the correct slice of the .xcframework for the current architectures
|
||||||
|
local target_path=""
|
||||||
|
|
||||||
|
# Split archs on space so we can find a slice that has all the needed archs
|
||||||
|
local target_archs=$(echo $ARCHS | tr " " "\n")
|
||||||
|
|
||||||
|
local target_variant=""
|
||||||
|
if [[ "$PLATFORM_NAME" == *"simulator" ]]; then
|
||||||
|
target_variant="simulator"
|
||||||
|
fi
|
||||||
|
if [[ ! -z ${EFFECTIVE_PLATFORM_NAME+x} && "$EFFECTIVE_PLATFORM_NAME" == *"maccatalyst" ]]; then
|
||||||
|
target_variant="maccatalyst"
|
||||||
|
fi
|
||||||
|
for i in ${!paths[@]}; do
|
||||||
|
local matched_all_archs="1"
|
||||||
|
local slice_archs="$(archs_for_slice "${xcframework_name}/${paths[$i]}")"
|
||||||
|
local slice_variant="$(variant_for_slice "${xcframework_name}/${paths[$i]}")"
|
||||||
|
for target_arch in $target_archs; do
|
||||||
|
if ! [[ "${slice_variant}" == "$target_variant" ]]; then
|
||||||
|
matched_all_archs="0"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! echo "${slice_archs}" | tr " " "\n" | grep -F -q -x "$target_arch"; then
|
||||||
|
matched_all_archs="0"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$matched_all_archs" == "1" ]]; then
|
||||||
|
# Found a matching slice
|
||||||
|
echo "Selected xcframework slice ${paths[$i]}"
|
||||||
|
SELECT_SLICE_RETVAL=${paths[$i]}
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
install_xcframework() {
|
||||||
|
local basepath="$1"
|
||||||
|
local name="$2"
|
||||||
|
local package_type="$3"
|
||||||
|
local paths=("${@:4}")
|
||||||
|
|
||||||
|
# Locate the correct slice of the .xcframework for the current architectures
|
||||||
|
select_slice "${basepath}" "${paths[@]}"
|
||||||
|
local target_path="$SELECT_SLICE_RETVAL"
|
||||||
|
if [[ -z "$target_path" ]]; then
|
||||||
|
echo "warning: [CP] $(basename ${basepath}): Unable to find matching slice in '${paths[@]}' for the current build architectures ($ARCHS) and platform (${EFFECTIVE_PLATFORM_NAME-${PLATFORM_NAME}})."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local source="$basepath/$target_path"
|
||||||
|
|
||||||
|
local destination="${PODS_XCFRAMEWORKS_BUILD_DIR}/${name}"
|
||||||
|
|
||||||
|
if [ ! -d "$destination" ]; then
|
||||||
|
mkdir -p "$destination"
|
||||||
|
fi
|
||||||
|
|
||||||
|
copy_dir "$source/" "$destination"
|
||||||
|
echo "Copied $source to $destination"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_xcframework "${PODS_ROOT}/JCore/jcore-noidfa-ios-5.4.2.xcframework" "JCore" "library" "ios-arm64" "ios-arm64_x86_64-simulator"
|
||||||
|
|
||||||
17
Pods/Target Support Files/JCore/JCore.debug.xcconfig
generated
Normal file
17
Pods/Target Support Files/JCore/JCore.debug.xcconfig
generated
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
|
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/JCore
|
||||||
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/JCore"
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers"
|
||||||
|
LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore"
|
||||||
|
OTHER_LDFLAGS = $(inherited) -l"JCore" -l"resolv" -l"z" -framework "CFNetwork" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreTelephony" -framework "Foundation" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||||
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||||
|
PODS_ROOT = ${SRCROOT}
|
||||||
|
PODS_TARGET_SRCROOT = ${PODS_ROOT}/JCore
|
||||||
|
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||||
|
SKIP_INSTALL = YES
|
||||||
|
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||||
|
VALID_ARCHS = x86_64 arm64
|
||||||
17
Pods/Target Support Files/JCore/JCore.release.xcconfig
generated
Normal file
17
Pods/Target Support Files/JCore/JCore.release.xcconfig
generated
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
|
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/JCore
|
||||||
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/JCore"
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers"
|
||||||
|
LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore"
|
||||||
|
OTHER_LDFLAGS = $(inherited) -l"JCore" -l"resolv" -l"z" -framework "CFNetwork" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreTelephony" -framework "Foundation" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||||
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||||
|
PODS_ROOT = ${SRCROOT}
|
||||||
|
PODS_TARGET_SRCROOT = ${PODS_ROOT}/JCore
|
||||||
|
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||||
|
SKIP_INSTALL = YES
|
||||||
|
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||||
|
VALID_ARCHS = x86_64 arm64
|
||||||
2
Pods/Target Support Files/JPush/JPush-xcframeworks-input-files.xcfilelist
generated
Normal file
2
Pods/Target Support Files/JPush/JPush-xcframeworks-input-files.xcfilelist
generated
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
${PODS_ROOT}/Target Support Files/JPush/JPush-xcframeworks.sh
|
||||||
|
${PODS_ROOT}/JPush/jpush-ios-6.1.0.xcframework
|
||||||
1
Pods/Target Support Files/JPush/JPush-xcframeworks-output-files.xcfilelist
generated
Normal file
1
Pods/Target Support Files/JPush/JPush-xcframeworks-output-files.xcfilelist
generated
Normal file
@ -0,0 +1 @@
|
|||||||
|
${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/jpush-ios-6.1.0.framework
|
||||||
121
Pods/Target Support Files/JPush/JPush-xcframeworks.sh
generated
Executable file
121
Pods/Target Support Files/JPush/JPush-xcframeworks.sh
generated
Executable file
@ -0,0 +1,121 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
set -u
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
function on_error {
|
||||||
|
echo "$(realpath -mq "${0}"):$1: error: Unexpected failure"
|
||||||
|
}
|
||||||
|
trap 'on_error $LINENO' ERR
|
||||||
|
|
||||||
|
|
||||||
|
# This protects against multiple targets copying the same framework dependency at the same time. The solution
|
||||||
|
# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html
|
||||||
|
RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????")
|
||||||
|
|
||||||
|
|
||||||
|
variant_for_slice()
|
||||||
|
{
|
||||||
|
case "$1" in
|
||||||
|
"jpush-ios-6.1.0.xcframework/ios-arm64")
|
||||||
|
echo ""
|
||||||
|
;;
|
||||||
|
"jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator")
|
||||||
|
echo "simulator"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
archs_for_slice()
|
||||||
|
{
|
||||||
|
case "$1" in
|
||||||
|
"jpush-ios-6.1.0.xcframework/ios-arm64")
|
||||||
|
echo "arm64"
|
||||||
|
;;
|
||||||
|
"jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator")
|
||||||
|
echo "arm64 x86_64"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_dir()
|
||||||
|
{
|
||||||
|
local source="$1"
|
||||||
|
local destination="$2"
|
||||||
|
|
||||||
|
# Use filter instead of exclude so missing patterns don't throw errors.
|
||||||
|
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" \"${source}*\" \"${destination}\""
|
||||||
|
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" "${source}"/* "${destination}"
|
||||||
|
}
|
||||||
|
|
||||||
|
SELECT_SLICE_RETVAL=""
|
||||||
|
|
||||||
|
select_slice() {
|
||||||
|
local xcframework_name="$1"
|
||||||
|
xcframework_name="${xcframework_name##*/}"
|
||||||
|
local paths=("${@:2}")
|
||||||
|
# Locate the correct slice of the .xcframework for the current architectures
|
||||||
|
local target_path=""
|
||||||
|
|
||||||
|
# Split archs on space so we can find a slice that has all the needed archs
|
||||||
|
local target_archs=$(echo $ARCHS | tr " " "\n")
|
||||||
|
|
||||||
|
local target_variant=""
|
||||||
|
if [[ "$PLATFORM_NAME" == *"simulator" ]]; then
|
||||||
|
target_variant="simulator"
|
||||||
|
fi
|
||||||
|
if [[ ! -z ${EFFECTIVE_PLATFORM_NAME+x} && "$EFFECTIVE_PLATFORM_NAME" == *"maccatalyst" ]]; then
|
||||||
|
target_variant="maccatalyst"
|
||||||
|
fi
|
||||||
|
for i in ${!paths[@]}; do
|
||||||
|
local matched_all_archs="1"
|
||||||
|
local slice_archs="$(archs_for_slice "${xcframework_name}/${paths[$i]}")"
|
||||||
|
local slice_variant="$(variant_for_slice "${xcframework_name}/${paths[$i]}")"
|
||||||
|
for target_arch in $target_archs; do
|
||||||
|
if ! [[ "${slice_variant}" == "$target_variant" ]]; then
|
||||||
|
matched_all_archs="0"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! echo "${slice_archs}" | tr " " "\n" | grep -F -q -x "$target_arch"; then
|
||||||
|
matched_all_archs="0"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$matched_all_archs" == "1" ]]; then
|
||||||
|
# Found a matching slice
|
||||||
|
echo "Selected xcframework slice ${paths[$i]}"
|
||||||
|
SELECT_SLICE_RETVAL=${paths[$i]}
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
install_xcframework() {
|
||||||
|
local basepath="$1"
|
||||||
|
local name="$2"
|
||||||
|
local package_type="$3"
|
||||||
|
local paths=("${@:4}")
|
||||||
|
|
||||||
|
# Locate the correct slice of the .xcframework for the current architectures
|
||||||
|
select_slice "${basepath}" "${paths[@]}"
|
||||||
|
local target_path="$SELECT_SLICE_RETVAL"
|
||||||
|
if [[ -z "$target_path" ]]; then
|
||||||
|
echo "warning: [CP] $(basename ${basepath}): Unable to find matching slice in '${paths[@]}' for the current build architectures ($ARCHS) and platform (${EFFECTIVE_PLATFORM_NAME-${PLATFORM_NAME}})."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local source="$basepath/$target_path"
|
||||||
|
|
||||||
|
local destination="${PODS_XCFRAMEWORKS_BUILD_DIR}/${name}"
|
||||||
|
|
||||||
|
if [ ! -d "$destination" ]; then
|
||||||
|
mkdir -p "$destination"
|
||||||
|
fi
|
||||||
|
|
||||||
|
copy_dir "$source/" "$destination"
|
||||||
|
echo "Copied $source to $destination"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_xcframework "${PODS_ROOT}/JPush/jpush-ios-6.1.0.xcframework" "JPush" "library" "ios-arm64" "ios-arm64_x86_64-simulator"
|
||||||
|
|
||||||
15
Pods/Target Support Files/JPush/JPush.debug.xcconfig
generated
Normal file
15
Pods/Target Support Files/JPush/JPush.debug.xcconfig
generated
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
|
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/JPush
|
||||||
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush"
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||||
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||||
|
PODS_ROOT = ${SRCROOT}
|
||||||
|
PODS_TARGET_SRCROOT = ${PODS_ROOT}/JPush
|
||||||
|
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||||
|
SKIP_INSTALL = YES
|
||||||
|
SWIFT_INCLUDE_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore"
|
||||||
|
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||||
15
Pods/Target Support Files/JPush/JPush.release.xcconfig
generated
Normal file
15
Pods/Target Support Files/JPush/JPush.release.xcconfig
generated
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
|
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/JPush
|
||||||
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush"
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||||
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||||
|
PODS_ROOT = ${SRCROOT}
|
||||||
|
PODS_TARGET_SRCROOT = ${PODS_ROOT}/JPush
|
||||||
|
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||||
|
SKIP_INSTALL = YES
|
||||||
|
SWIFT_INCLUDE_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore"
|
||||||
|
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||||
@ -219,6 +219,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
## JCore
|
||||||
|
|
||||||
|
Copyright jpush.cn
|
||||||
|
|
||||||
|
## JPush
|
||||||
|
|
||||||
|
Copyright jiguang.cn
|
||||||
|
|
||||||
## UMAPM
|
## UMAPM
|
||||||
|
|
||||||
Copyright 2011 - 2020 umeng.com. All rights reserved.
|
Copyright 2011 - 2020 umeng.com. All rights reserved.
|
||||||
|
|||||||
@ -302,6 +302,26 @@ SOFTWARE.
|
|||||||
<key>Type</key>
|
<key>Type</key>
|
||||||
<string>PSGroupSpecifier</string>
|
<string>PSGroupSpecifier</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>FooterText</key>
|
||||||
|
<string>Copyright jpush.cn</string>
|
||||||
|
<key>License</key>
|
||||||
|
<string>Copyright</string>
|
||||||
|
<key>Title</key>
|
||||||
|
<string>JCore</string>
|
||||||
|
<key>Type</key>
|
||||||
|
<string>PSGroupSpecifier</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>FooterText</key>
|
||||||
|
<string>Copyright jiguang.cn</string>
|
||||||
|
<key>License</key>
|
||||||
|
<string>Copyright</string>
|
||||||
|
<key>Title</key>
|
||||||
|
<string>JPush</string>
|
||||||
|
<key>Type</key>
|
||||||
|
<string>PSGroupSpecifier</string>
|
||||||
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>FooterText</key>
|
<key>FooterText</key>
|
||||||
<string>Copyright 2011 - 2020 umeng.com. All rights reserved.
|
<string>Copyright 2011 - 2020 umeng.com. All rights reserved.
|
||||||
|
|||||||
@ -3,13 +3,13 @@ ARCHS = $(ARCHS_STANDARD)
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
||||||
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
||||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers"
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
|
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush" "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||||
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "AMapFoundationKit" -framework "AMapLocationKit" -framework "AMapSearchKit" -framework "Combine" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "MAMapKit" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "UMAPM" -framework "UMCommon" -framework "UMDevice" -framework "WebKit" -framework "WechatOpenSDK"
|
OTHER_LDFLAGS = $(inherited) -ObjC -l"JCore" -l"JPush" -l"c++" -l"resolv" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "AMapFoundationKit" -framework "AMapLocationKit" -framework "AMapSearchKit" -framework "CFNetwork" -framework "Combine" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "Foundation" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "MAMapKit" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "UMAPM" -framework "UMCommon" -framework "UMDevice" -framework "WebKit" -framework "WechatOpenSDK" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AMap3DMap-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapFoundation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapLocation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapSearch-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMAPM" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMCommon" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMDevice" "-F${PODS_CONFIGURATION_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AMap3DMap-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapFoundation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapLocation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapSearch-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "-F${PODS_CONFIGURATION_BUILD_DIR}/JCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/JPush" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMAPM" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMCommon" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMDevice" "-F${PODS_CONFIGURATION_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
|||||||
@ -3,13 +3,13 @@ ARCHS = $(ARCHS_STANDARD)
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
||||||
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
||||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers"
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
|
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush" "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||||
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "AMapFoundationKit" -framework "AMapLocationKit" -framework "AMapSearchKit" -framework "Combine" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "MAMapKit" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "UMAPM" -framework "UMCommon" -framework "UMDevice" -framework "WebKit" -framework "WechatOpenSDK"
|
OTHER_LDFLAGS = $(inherited) -ObjC -l"JCore" -l"JPush" -l"c++" -l"resolv" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "AMapFoundationKit" -framework "AMapLocationKit" -framework "AMapSearchKit" -framework "CFNetwork" -framework "Combine" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "Foundation" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "MAMapKit" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "UMAPM" -framework "UMCommon" -framework "UMDevice" -framework "WebKit" -framework "WechatOpenSDK" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AMap3DMap-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapFoundation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapLocation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapSearch-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMAPM" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMCommon" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMDevice" "-F${PODS_CONFIGURATION_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AMap3DMap-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapFoundation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapLocation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapSearch-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "-F${PODS_CONFIGURATION_BUILD_DIR}/JCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/JPush" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMAPM" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMCommon" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMDevice" "-F${PODS_CONFIGURATION_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
|||||||
@ -2,10 +2,10 @@ ARCHS = $(ARCHS_STANDARD)
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
||||||
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
||||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers"
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||||
OTHER_LDFLAGS = $(inherited) -l"c++" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "Combine" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit"
|
OTHER_LDFLAGS = $(inherited) -l"c++" -l"resolv" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "CFNetwork" -framework "Combine" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "Foundation" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
||||||
|
|||||||
@ -2,10 +2,10 @@ ARCHS = $(ARCHS_STANDARD)
|
|||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
||||||
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
||||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers"
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||||
OTHER_LDFLAGS = $(inherited) -l"c++" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "Combine" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit"
|
OTHER_LDFLAGS = $(inherited) -l"c++" -l"resolv" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "CFNetwork" -framework "Combine" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "Foundation" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
||||||
|
|||||||
484
docs/后端消息推送格式.md
Normal file
484
docs/后端消息推送格式.md
Normal file
@ -0,0 +1,484 @@
|
|||||||
|
# 后端消息推送格式
|
||||||
|
|
||||||
|
## 1. 文档说明
|
||||||
|
|
||||||
|
- 本文整理后端经极光推送发送到 Android 和 iOS 的通知消息格式。
|
||||||
|
- 当前内容基于截至 2026-07-23 的 Android、iOS 实际收包日志、两端客户端现有解析逻辑,以及后端开发人员提供的 `PushMsg` 类型定义。
|
||||||
|
- 后端当前定义了 `1`~`13`、`100`、`999` 共 15 个业务消息类型;目前已取得 `type = 1`(付款成功)、`type = 2`(退款成功)、`type = 6`(扫码成功)和 `type = 13`(新获客线索)的收包样本。
|
||||||
|
- 本文描述的是推送协议,不是普通 HTTP 接口响应格式。
|
||||||
|
|
||||||
|
## 2. 统一业务结构
|
||||||
|
|
||||||
|
极光在 Android 和 iOS 上交付的原始结构不同,但当前消息都可以归一化为以下业务结构:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "通知标题",
|
||||||
|
"content": "通知正文",
|
||||||
|
"msg_id": 1858,
|
||||||
|
"type": 6,
|
||||||
|
"data": {
|
||||||
|
"order_type": 14
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.1 公共业务字段
|
||||||
|
|
||||||
|
| 字段 | 当前类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `title` | string | 通知标题。Android 为 `notificationTitle`,iOS 为 `aps.alert.title`。 |
|
||||||
|
| `content` | string | 通知正文。Android 为 `notificationContent`,iOS 为 `aps.alert.body`。 |
|
||||||
|
| `msg_id` | int | 后端业务消息 ID。它与极光消息 ID 不是同一个字段。 |
|
||||||
|
| `type` | int | 业务消息类型,是客户端业务分发的主要依据。 |
|
||||||
|
| `data` | object | 随消息类型变化的业务数据。 |
|
||||||
|
|
||||||
|
> “当前类型”表示现有样本中观察到的 JSON 类型。后端应保持字段类型稳定,避免同一个字段在不同推送中交替使用数字、字符串或 `null`。
|
||||||
|
|
||||||
|
## 3. Android 收包格式
|
||||||
|
|
||||||
|
Android 通过极光 `JPushMessageReceiver.onNotifyMessageArrived` 收到通知。标题、正文和附加字段分别由 SDK 提供,其中 `extras` 是 JSON 字符串:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[onNotifyMessageArrived] msgId=<极光消息ID>, title=<标题>, content=<正文>, extras=<业务JSON字符串>
|
||||||
|
```
|
||||||
|
|
||||||
|
`extras` 的当前结构为:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {},
|
||||||
|
"msg_id": 0,
|
||||||
|
"type": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.1 扫码成功原始日志
|
||||||
|
|
||||||
|
```text
|
||||||
|
[onNotifyMessageArrived] msgId=18103321643292031, title=扫码成功通知, content=扫码成功, extras={"data":{"order_type":14},"msg_id":1852,"type":6}
|
||||||
|
```
|
||||||
|
|
||||||
|
对应的 `extras`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"order_type": 14
|
||||||
|
},
|
||||||
|
"msg_id": 1852,
|
||||||
|
"type": 6
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 付款成功原始日志
|
||||||
|
|
||||||
|
```text
|
||||||
|
[onNotifyMessageArrived] msgId=18103321644290274, title=付款成功通知, content=您的订单 #260722128005 已支付成功,支付金额 ¥0.01, extras={"data":{"order_amount":"0.01","order_number":"260722128005","order_type":14,"pay_time":1784711036,"remark":""},"msg_id":1853,"type":1}
|
||||||
|
```
|
||||||
|
|
||||||
|
对应的 `extras`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"order_amount": "0.01",
|
||||||
|
"order_number": "260722128005",
|
||||||
|
"order_type": 14,
|
||||||
|
"pay_time": 1784711036,
|
||||||
|
"remark": ""
|
||||||
|
},
|
||||||
|
"msg_id": 1853,
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 退款成功原始日志
|
||||||
|
|
||||||
|
```text
|
||||||
|
[onNotifyMessageArrived] msgId=18103321800527159, title=退款成功通知, content=您的订单 #260722128002 已成功退款,退款金额 ¥0.01, extras={"data":{},"msg_id":1867,"type":2}
|
||||||
|
```
|
||||||
|
|
||||||
|
对应的 `extras`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {},
|
||||||
|
"msg_id": 1867,
|
||||||
|
"type": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. iOS 收包格式
|
||||||
|
|
||||||
|
iOS 通过 APNs/极光收到 `userInfo` 字典。与 Android 相比:
|
||||||
|
|
||||||
|
- 标题和正文位于 `aps.alert`。
|
||||||
|
- Android `extras` 中的 `msg_id`、`type`、`data` 在 iOS 中位于 payload 顶层。
|
||||||
|
- `_j_*` 为极光传输层内部字段,不应作为业务分发依据。
|
||||||
|
|
||||||
|
通用结构如下:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_j_business": 1,
|
||||||
|
"_j_data": "{\"data_msgtype\":1,\"push_type\":8,\"is_vip\":0}",
|
||||||
|
"_j_msgid": 18103321726881995,
|
||||||
|
"_j_uid": 86399144805,
|
||||||
|
"aps": {
|
||||||
|
"alert": {
|
||||||
|
"body": "通知正文",
|
||||||
|
"title": "通知标题"
|
||||||
|
},
|
||||||
|
"sound": "default"
|
||||||
|
},
|
||||||
|
"data": {},
|
||||||
|
"msg_id": 0,
|
||||||
|
"type": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 原始日志中该内部字段显示为 `*j\_data*`。本文按极光其他内部字段的命名方式记为 `_j_data`;后续联调时应以设备收到的真实字典 key 为准。该字段目前不参与业务解析。
|
||||||
|
|
||||||
|
### 4.1 扫码成功 payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_j_business": 1,
|
||||||
|
"_j_data": "{\"data_msgtype\":1,\"push_type\":8,\"is_vip\":0}",
|
||||||
|
"_j_msgid": 18103321726881995,
|
||||||
|
"_j_uid": 86399144805,
|
||||||
|
"aps": {
|
||||||
|
"alert": {
|
||||||
|
"body": "扫码成功",
|
||||||
|
"title": "扫码成功通知"
|
||||||
|
},
|
||||||
|
"sound": "default"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"order_type": 14
|
||||||
|
},
|
||||||
|
"msg_id": 1858,
|
||||||
|
"type": 6
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 付款成功 payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_j_business": 1,
|
||||||
|
"_j_data": "{\"data_msgtype\":1,\"push_type\":8,\"is_vip\":0}",
|
||||||
|
"_j_msgid": 18103321720587147,
|
||||||
|
"_j_uid": 86399144805,
|
||||||
|
"aps": {
|
||||||
|
"alert": {
|
||||||
|
"body": "您的订单 #260722128006 已支付成功,支付金额 ¥0.01",
|
||||||
|
"title": "付款成功通知"
|
||||||
|
},
|
||||||
|
"sound": "default"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"order_amount": "0.01",
|
||||||
|
"order_number": "260722128006",
|
||||||
|
"order_type": 14,
|
||||||
|
"pay_time": 1784712671,
|
||||||
|
"remark": ""
|
||||||
|
},
|
||||||
|
"msg_id": 1856,
|
||||||
|
"type": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 退款成功 payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_j_business": 1,
|
||||||
|
"_j_data": "{\"data_msgtype\":1,\"push_type\":8,\"is_vip\":0}",
|
||||||
|
"_j_msgid": 18103321792603316,
|
||||||
|
"_j_uid": 86399144805,
|
||||||
|
"aps": {
|
||||||
|
"alert": {
|
||||||
|
"body": "您的订单 #260722128005 已成功退款,退款金额 ¥0.01",
|
||||||
|
"title": "退款成功通知"
|
||||||
|
},
|
||||||
|
"sound": "default"
|
||||||
|
},
|
||||||
|
"data": {},
|
||||||
|
"msg_id": 1862,
|
||||||
|
"type": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 新获客线索 payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_j_business": 1,
|
||||||
|
"_j_data": "{\"data_msgtype\":1,\"push_type\":8,\"is_vip\":0}",
|
||||||
|
"_j_msgid": 18103323696468714,
|
||||||
|
"_j_uid": 86399144805,
|
||||||
|
"aps": {
|
||||||
|
"alert": {
|
||||||
|
"body": "获客员哦啦啦提交了一条新线索,请及时查看并跟进",
|
||||||
|
"title": "新获客线索通知"
|
||||||
|
},
|
||||||
|
"sound": "default"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"lead_id": 75,
|
||||||
|
"sale_user_id": 6,
|
||||||
|
"saler_name": "哦啦啦"
|
||||||
|
},
|
||||||
|
"msg_id": 1869,
|
||||||
|
"type": 13
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Android 与 iOS 字段对应关系
|
||||||
|
|
||||||
|
| 语义 | Android | iOS | 是否用于业务 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 极光消息 ID | SDK `message.msgId` | `_j_msgid` | 否,仅用于推送链路日志与排查 |
|
||||||
|
| 通知标题 | SDK `message.notificationTitle` | `aps.alert.title` | 是,用于展示 |
|
||||||
|
| 通知正文 | SDK `message.notificationContent` | `aps.alert.body` | 是,用于展示 |
|
||||||
|
| 后端业务消息 ID | `extras.msg_id` | 顶层 `msg_id` | 是 |
|
||||||
|
| 业务消息类型 | `extras.type` | 顶层 `type` | 是,作为业务分发主键 |
|
||||||
|
| 业务数据 | `extras.data` | 顶层 `data` | 是,结构由 `type` 决定 |
|
||||||
|
| 极光业务标记 | SDK 内部处理 | `_j_business` | 否 |
|
||||||
|
| 极光附加元数据 | SDK 内部处理 | `_j_data` | 否 |
|
||||||
|
| 极光用户 ID | SDK 内部处理 | `_j_uid` | 否 |
|
||||||
|
| APNs 展示配置 | 不适用 | `aps` | 仅 iOS 系统使用 |
|
||||||
|
|
||||||
|
### 5.1 两种消息 ID 不可混用
|
||||||
|
|
||||||
|
- `msgId` / `_j_msgid`:极光生成的传输层消息 ID,当前样本为 17 位数字。
|
||||||
|
- `msg_id`:后端生成的业务消息 ID,当前样本为 `1852`、`1853`、`1856`、`1858`。
|
||||||
|
- 客户端日志、去重或上报时必须明确需要哪一种 ID,不能仅用名称相近而互相替代。
|
||||||
|
- 极光消息 ID 已超过 JavaScript 可安全表示的整数范围;若需要经过 JavaScript、WebView 或其他只能安全处理 53 位整数的链路,应按字符串传递。
|
||||||
|
|
||||||
|
## 6. 消息类型清单
|
||||||
|
|
||||||
|
### 6.1 后端 `PushMsg` 完整类型定义
|
||||||
|
|
||||||
|
定义位置:后端 `PushMsg.php` 第 49 行附近。
|
||||||
|
|
||||||
|
| `type` | 消息类型 | 后端备注 | 当前收包样本 |
|
||||||
|
| ---: | --- | --- | --- |
|
||||||
|
| `1` | 付款成功通知 | 上文实际示例即为此类型 | Android、iOS 均已取得 |
|
||||||
|
| `2` | 退款成功通知 | — | Android、iOS 均已取得 |
|
||||||
|
| `3` | 下单成功通知 | App 无需推送,当前未使用 | 暂无 |
|
||||||
|
| `4` | 签到成功通知 | — | 暂无 |
|
||||||
|
| `5` | 付尾款成功通知 | — | 暂无 |
|
||||||
|
| `6` | 扫码成功通知 | — | Android、iOS 均已取得 |
|
||||||
|
| `7` | 付定金成功通知 | — | 暂无 |
|
||||||
|
| `8` | 订单核销成功通知 | — | 暂无 |
|
||||||
|
| `9` | 剪辑完成通知 | — | 暂无 |
|
||||||
|
| `10` | 修图完成通知 | — | 暂无 |
|
||||||
|
| `11` | 跟拍签到通知 | — | 暂无 |
|
||||||
|
| `12` | 剪辑师新任务通知 | 类型名称映射中漏配 | 暂无 |
|
||||||
|
| `13` | 新获客线索通知 | 获客员提交新线索 | iOS 已取得,Android 暂无 |
|
||||||
|
| `100` | 用户登出通知 | 账号在其他设备登录 | 暂无 |
|
||||||
|
| `999` | 系统消息 | 当前未使用 | 暂无 |
|
||||||
|
|
||||||
|
后端特别说明:
|
||||||
|
|
||||||
|
- `type` 表示业务消息类型,不代表推送渠道。
|
||||||
|
- 推送渠道的取值另有定义:`1` 为极光推送,`2` 为微信小程序,`999` 为不推送。
|
||||||
|
- `type = 12` 虽然已经定义,但尚未加入后端 `MAP_TYPE_NAMES`。后端调用 `getTypeName(12)` 时会返回“未知消息类型”,需要后端补充映射。
|
||||||
|
- “暂无收包样本”只表示当前文档还没有该类型的完整 payload,不能据此认为该类型未启用。收到后应继续补充其标题、正文、`data` 字段和点击行为。
|
||||||
|
|
||||||
|
### 6.2 `type = 1`:付款成功
|
||||||
|
|
||||||
|
| 项目 | 当前值 |
|
||||||
|
| --- | --- |
|
||||||
|
| 标题示例 | `付款成功通知` |
|
||||||
|
| 正文示例 | `您的订单 #260722128006 已支付成功,支付金额 ¥0.01` |
|
||||||
|
| 客户端点击行为 | Android、iOS 均进入收款记录页 |
|
||||||
|
|
||||||
|
`data` 字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 当前样本 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `order_amount` | string | `"0.01"` | 支付金额。使用十进制字符串,避免浮点精度问题。 |
|
||||||
|
| `order_number` | string | `"260722128006"` | 业务订单号。 |
|
||||||
|
| `order_type` | int | `14` | 订单类型;当前值表示“摄影师跟拍线下扫码”。 |
|
||||||
|
| `pay_time` | int | `1784712671` | 支付时间。当前样本为 Unix 秒级时间戳;Android 现有代码同时兼容秒和毫秒。 |
|
||||||
|
| `remark` | string | `""` | 付款备注;没有备注时当前返回空字符串。 |
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"msg_id": 1856,
|
||||||
|
"data": {
|
||||||
|
"order_amount": "0.01",
|
||||||
|
"order_number": "260722128006",
|
||||||
|
"order_type": 14,
|
||||||
|
"pay_time": 1784712671,
|
||||||
|
"remark": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 `type = 2`:退款成功
|
||||||
|
|
||||||
|
| 项目 | 当前值 |
|
||||||
|
| --- | --- |
|
||||||
|
| 标题示例 | `退款成功通知` |
|
||||||
|
| 正文示例 | `您的订单 #260722128005 已成功退款,退款金额 ¥0.01` |
|
||||||
|
| 客户端点击行为 | Android、iOS 均进入消息中心 |
|
||||||
|
|
||||||
|
`data` 字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 当前样本 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| — | object | `{}` | 当前 Android、iOS 样本均为空对象。 |
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": 2,
|
||||||
|
"msg_id": 1862,
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 当前订单号和退款金额只存在于展示正文中,没有以结构化字段放入 `data`。客户端如果需要可靠地刷新指定订单、展示退款详情或执行金额计算,应由后端在 `data` 中提供字段,不应解析自然语言正文。
|
||||||
|
|
||||||
|
### 6.4 `type = 6`:扫码成功
|
||||||
|
|
||||||
|
| 项目 | 当前值 |
|
||||||
|
| --- | --- |
|
||||||
|
| 标题示例 | `扫码成功通知` |
|
||||||
|
| 正文示例 | `扫码成功` |
|
||||||
|
| 客户端点击行为 | Android、iOS 均进入收款详情页 |
|
||||||
|
|
||||||
|
`data` 字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 当前样本 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `order_type` | int | `14` | 订单类型;`14` 在 Android 现有代码中表示“摄影师跟拍线下扫码”。 |
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": 6,
|
||||||
|
"msg_id": 1858,
|
||||||
|
"data": {
|
||||||
|
"order_type": 14
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.5 `type = 13`:新获客线索
|
||||||
|
|
||||||
|
| 项目 | 当前值 |
|
||||||
|
| --- | --- |
|
||||||
|
| 标题示例 | `新获客线索通知` |
|
||||||
|
| 正文示例 | `获客员哦啦啦提交了一条新线索,请及时查看并跟进` |
|
||||||
|
| 客户端点击行为 | Android、iOS 均进入消息中心 |
|
||||||
|
|
||||||
|
`data` 字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 当前样本 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `lead_id` | int | `75` | 新增获客线索的唯一 ID。 |
|
||||||
|
| `sale_user_id` | int | `6` | 提交线索的获客员 ID。 |
|
||||||
|
| `saler_name` | string | `"哦啦啦"` | 获客员名称,与通知正文中的名称一致。 |
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": 13,
|
||||||
|
"msg_id": 1869,
|
||||||
|
"data": {
|
||||||
|
"lead_id": 75,
|
||||||
|
"sale_user_id": 6,
|
||||||
|
"saler_name": "哦啦啦"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 当前点击路由只根据 `type` 判断,`type = 13` 属于默认分支,因此进入消息中心;`data` 中的线索字段不会改变跳转页面。
|
||||||
|
|
||||||
|
## 7. 客户端解析约定
|
||||||
|
|
||||||
|
1. Android、iOS 的通知点击路由只根据业务字段 `type` 判断,不读取标题、正文、`data`、`route`、`uri`、`action` 或极光内部字段决定页面。
|
||||||
|
2. `type = 1`:进入收款记录页。
|
||||||
|
3. `type = 6`:进入收款详情页。
|
||||||
|
4. 其他任意 `type`、缺少 `type`、类型无法解析或 payload 损坏:进入消息中心。
|
||||||
|
5. Android 从 `notificationExtras` 或厂商通道包装字段中提取 `type`;iOS 优先读取 payload 顶层 `type`,并兼容常见推送包装层中的 `type`。
|
||||||
|
6. `data` 仍可按消息类型用于页面内容刷新,但不参与通知点击路由。新增字段应保持向后兼容,客户端应忽略当前版本不认识的额外字段。
|
||||||
|
7. 金额继续使用字符串;时间戳建议统一为 Unix 秒级整数,并在协议中固定单位。
|
||||||
|
8. `remark` 等可空业务字段建议固定返回空字符串或明确约定 `null`,不要在多种表示之间切换。
|
||||||
|
|
||||||
|
## 8. 新增消息类型记录模板
|
||||||
|
|
||||||
|
后续收到新类型时,复制以下内容追加到“消息类型清单”:
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
### 6.x `type = <类型值>`:<消息名称>
|
||||||
|
|
||||||
|
| 项目 | 当前值 |
|
||||||
|
| --- | --- |
|
||||||
|
| 首次发现日期 | `YYYY-MM-DD` |
|
||||||
|
| 标题示例 | `<title>` |
|
||||||
|
| 正文示例 | `<content>` |
|
||||||
|
| 客户端业务行为 | `<展示、刷新或跳转行为>` |
|
||||||
|
|
||||||
|
`data` 字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必有 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `<field>` | `<type>` | `<是/否/待确认>` | `<说明>` |
|
||||||
|
|
||||||
|
Android `extras` 示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {},
|
||||||
|
"msg_id": 0,
|
||||||
|
"type": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
iOS 业务字段示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {},
|
||||||
|
"msg_id": 0,
|
||||||
|
"type": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
记录新类型时至少保留以下信息:
|
||||||
|
|
||||||
|
- Android 和 iOS 各一份原始收包日志。
|
||||||
|
- `type`、`msg_id` 和完整 `data`。
|
||||||
|
- 标题、正文以及消息点击后的期望页面。
|
||||||
|
- 每个字段的 JSON 类型、是否必有、空值表示方式和时间/金额单位。
|
||||||
|
- 同一业务事件在两端的字段是否一致。
|
||||||
|
|
||||||
|
## 9. 待后端确认项
|
||||||
|
|
||||||
|
| 项目 | 当前观察 | 需要确认 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `msg_id` | 与极光消息 ID 不同 | 是否对应消息中心记录 ID,以及是否用于已读/送达状态上报 |
|
||||||
|
| `data` | `type = 1`、`2`、`6`、`13` 的样本中都存在;`type = 2` 为 `{}` | 所有业务推送是否保证存在;无业务字段时是否统一返回 `{}` |
|
||||||
|
| 退款成功的 `data` | 当前为空对象,订单号和退款金额只在正文中 | 是否补充 `order_number`、`refund_amount`、`refund_time`、`order_type` 等结构化字段 |
|
||||||
|
| 其他类型的 `data` | 后端已定义消息名称,但尚未提供 payload | 分别确认 `type = 3`~`5`、`7`~`12`、`100`、`999` 的字段结构、必填项及点击行为 |
|
||||||
|
| `pay_time` | 当前为秒级时间戳 | 后端是否统一保证使用 Unix 秒,且时区语义固定为 UTC 时间戳 |
|
||||||
|
| `remark` | 无备注时为空字符串 | 是否可能返回 `null` 或省略 |
|
||||||
|
| `order_type` | 当前为 `14` | 完整枚举和值含义由哪份接口文档维护 |
|
||||||
|
| 标题与正文 | 当前均存在 | 是否为所有通知消息的必有字段 |
|
||||||
|
| 推送渠道字段 | 后端已给出渠道取值 `1`、`2`、`999` | payload 或发送接口中对应字段的准确名称 |
|
||||||
|
| `type = 12` | `MAP_TYPE_NAMES` 中漏配 | 后端补充映射,避免 `getTypeName(12)` 返回“未知消息类型” |
|
||||||
|
| 新增 `type` | 当前定义维护在 `PushMsg.php` | 后端新增类型前是否可同步类型值、字段结构和客户端行为 |
|
||||||
204
docs/消息中心未读数量与全部已读接口需求.md
Normal file
204
docs/消息中心未读数量与全部已读接口需求.md
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
# 消息中心未读数量与全部已读接口需求
|
||||||
|
|
||||||
|
## 1. 需求背景
|
||||||
|
|
||||||
|
iOS App 需要实现以下功能:
|
||||||
|
|
||||||
|
1. 首页展示消息中心入口,有未读消息时显示红点。
|
||||||
|
2. 获取最新未读消息数量后,同步更新 App 桌面图标右上角的角标数量。
|
||||||
|
3. 用户点击某条未读消息并成功标记已读后,立即更新最新未读数量和桌面角标。
|
||||||
|
4. 消息中心导航栏右上角提供“全部已读”按钮。
|
||||||
|
5. 全部标记已读成功后,更新首页红点,并将桌面角标更新为服务端返回的最新未读数量。
|
||||||
|
|
||||||
|
## 2. 当前已有接口
|
||||||
|
|
||||||
|
### 2.1 获取消息列表
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/app/msg/list
|
||||||
|
```
|
||||||
|
|
||||||
|
当前参数:
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `last_id` | int | 否 | 分页游标,首次请求传 `0` |
|
||||||
|
| `limit` | int | 否 | 每页数量 |
|
||||||
|
| `unread` | int | 否 | `0` 获取全部消息,`1` 只获取未读消息 |
|
||||||
|
|
||||||
|
该接口仅返回分页消息,没有返回未读消息总数。客户端若通过分页遍历全部未读消息来统计数量,请求次数和数据流量都会随着未读消息数量增长,因此不适合作为桌面角标的常规数据来源。
|
||||||
|
|
||||||
|
### 2.2 标记单条消息已读
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/app/msg/read?id={message_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
当前接口可以标记单条消息已读,但成功响应没有返回操作后的最新未读数量。
|
||||||
|
|
||||||
|
## 3. 需要新增或调整的接口
|
||||||
|
|
||||||
|
### 3.1 获取未读消息总数
|
||||||
|
|
||||||
|
建议新增:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/app/msg/unread-count
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 请求说明
|
||||||
|
|
||||||
|
- 不需要业务请求参数。
|
||||||
|
- 根据当前登录 Token 识别用户及业务账号。
|
||||||
|
- 返回当前账号下尚未读取的消息总数。
|
||||||
|
|
||||||
|
#### 成功响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 100000,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"unread_count": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必有 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `unread_count` | int | 是 | 当前账号的最新未读消息数量,最小值为 `0` |
|
||||||
|
|
||||||
|
### 3.2 全部标记为已读
|
||||||
|
|
||||||
|
建议新增:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/app/msg/read-all
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 请求说明
|
||||||
|
|
||||||
|
- 不需要业务请求参数和请求体。
|
||||||
|
- 根据当前登录 Token 识别用户及业务账号。
|
||||||
|
- 将当前账号下请求执行时已经存在的所有未读消息标记为已读。
|
||||||
|
- 接口需要支持幂等调用;没有未读消息时仍返回成功。
|
||||||
|
|
||||||
|
#### 成功响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 100000,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"updated_count": 12,
|
||||||
|
"unread_count": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必有 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `updated_count` | int | 是 | 本次实际被更新为已读的消息数量 |
|
||||||
|
| `unread_count` | int | 是 | 操作完成后的最新未读消息数量 |
|
||||||
|
|
||||||
|
> 客户端应以响应中的 `unread_count` 为准,不应直接假定其一定为 `0`。例如“全部已读”执行期间如果有新消息到达,操作完成后仍可能存在新的未读消息。
|
||||||
|
|
||||||
|
### 3.3 调整单条已读接口响应
|
||||||
|
|
||||||
|
保留现有接口:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/app/msg/read?id={message_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
建议成功响应增加最新未读数量:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 100000,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"unread_count": 11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必有 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `unread_count` | int | 是 | 单条消息标记已读后的最新未读消息数量 |
|
||||||
|
|
||||||
|
#### 幂等要求
|
||||||
|
|
||||||
|
- 对已经处于已读状态的消息重复调用时,接口仍返回成功和最新 `unread_count`。
|
||||||
|
- 消息不存在或不属于当前账号时,返回明确的业务错误,不能修改其他账号的消息。
|
||||||
|
|
||||||
|
## 4. 统一约定
|
||||||
|
|
||||||
|
### 4.1 账号范围
|
||||||
|
|
||||||
|
- 所有接口必须根据登录 Token 限定当前用户及当前业务账号的数据范围。
|
||||||
|
- 景区账号、门店账号等不同业务账号之间的未读数量不能互相污染。
|
||||||
|
- 切换账号后,客户端会重新调用未读数量接口。
|
||||||
|
|
||||||
|
### 4.2 数值约束
|
||||||
|
|
||||||
|
- `unread_count` 和 `updated_count` 必须返回 JSON 整数。
|
||||||
|
- 数值不得为负数。
|
||||||
|
- 即使数量为 `0`,字段也必须存在,不能返回 `null`、空字符串或省略字段。
|
||||||
|
|
||||||
|
### 4.3 一致性与并发
|
||||||
|
|
||||||
|
- 单条已读或全部已读操作及其响应中的 `unread_count` 应基于同一账号范围。
|
||||||
|
- `read-all` 应尽量在事务内完成批量更新和剩余未读数统计。
|
||||||
|
- 如果操作期间有新消息写入,响应应返回操作完成时数据库中的真实 `unread_count`。
|
||||||
|
- 客户端始终以最近一次成功接口响应中的 `unread_count` 更新首页红点和桌面角标。
|
||||||
|
|
||||||
|
### 4.4 错误响应
|
||||||
|
|
||||||
|
沿用现有统一业务响应结构,例如:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 100001,
|
||||||
|
"msg": "消息不存在或无权操作",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
建议至少区分以下情况:
|
||||||
|
|
||||||
|
- Token 无效或登录已过期。
|
||||||
|
- 单条消息不存在。
|
||||||
|
- 单条消息不属于当前账号。
|
||||||
|
- 数据库更新失败。
|
||||||
|
|
||||||
|
## 5. 客户端调用时机
|
||||||
|
|
||||||
|
| 场景 | 调用接口 | 客户端处理 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 登录后首次进入首页 | `GET /msg/unread-count` | 更新首页红点和桌面角标 |
|
||||||
|
| App 回到前台 | `GET /msg/unread-count` | 校准最新未读数量 |
|
||||||
|
| 前台收到新消息推送 | `GET /msg/unread-count` | 更新首页红点和桌面角标 |
|
||||||
|
| 点击一条未读消息 | `POST /msg/read` | 使用返回的 `unread_count` 更新红点和桌面角标 |
|
||||||
|
| 点击“全部已读” | `POST /msg/read-all` | 刷新消息列表,使用返回的 `unread_count` 更新红点和桌面角标 |
|
||||||
|
| 切换业务账号 | `GET /msg/unread-count` | 使用新账号的数量覆盖旧账号角标 |
|
||||||
|
| 退出登录 | 无 | 客户端将桌面角标清零 |
|
||||||
|
|
||||||
|
## 6. 验收标准
|
||||||
|
|
||||||
|
1. 未读数量为 `0` 时,接口稳定返回 `{"unread_count": 0}`。
|
||||||
|
2. 存在多页未读消息时,未读数量接口返回完整总数,而不是当前页数量。
|
||||||
|
3. 单条未读消息标记成功后,返回的 `unread_count` 与数据库实际剩余未读数一致。
|
||||||
|
4. 对同一消息重复调用单条已读接口不会重复扣减未读数量。
|
||||||
|
5. 全部已读接口可一次处理当前账号的所有未读消息,并返回实际更新数量。
|
||||||
|
6. 重复调用全部已读接口仍成功,返回 `updated_count = 0`。
|
||||||
|
7. 不同业务账号的未读数量相互隔离。
|
||||||
|
8. 并发到达新消息时,响应中的 `unread_count` 能反映操作完成时的真实剩余数量。
|
||||||
|
9. 所有接口继续使用项目现有统一鉴权、业务状态码和响应信封格式。
|
||||||
|
|
||||||
@ -208,6 +208,11 @@
|
|||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
93FF17C42FFB84950033C304 = {
|
93FF17C42FFB84950033C304 = {
|
||||||
CreatedOnToolsVersion = 26.6;
|
CreatedOnToolsVersion = 26.6;
|
||||||
|
SystemCapabilities = {
|
||||||
|
com.apple.Push = {
|
||||||
|
enabled = 1;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
A1B2C3D32FFB84950033C305 = {
|
A1B2C3D32FFB84950033C305 = {
|
||||||
CreatedOnToolsVersion = 26.6;
|
CreatedOnToolsVersion = 26.6;
|
||||||
@ -422,6 +427,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 5BBB56C2281879245220A9F3 /* Pods-suixinkan.debug.xcconfig */;
|
baseConfigurationReference = 5BBB56C2281879245220A9F3 /* Pods-suixinkan.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
APS_ENVIRONMENT = development;
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
|
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
|
||||||
@ -487,6 +493,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 1060D5E389506965D83EA1CC /* Pods-suixinkan.release.xcconfig */;
|
baseConfigurationReference = 1060D5E389506965D83EA1CC /* Pods-suixinkan.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
APS_ENVIRONMENT = production;
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
|
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
|
||||||
|
|||||||
@ -29,6 +29,7 @@ final class NetworkServices {
|
|||||||
let liveAPI: LiveAPI
|
let liveAPI: LiveAPI
|
||||||
let cloudDriveAPI: CloudDriveAPI
|
let cloudDriveAPI: CloudDriveAPI
|
||||||
let messageCenterAPI: MessageCenterAPI
|
let messageCenterAPI: MessageCenterAPI
|
||||||
|
let pushAPI: PushAPI
|
||||||
let wildPhotographerReportAPI: WildPhotographerReportAPI
|
let wildPhotographerReportAPI: WildPhotographerReportAPI
|
||||||
let uploadAPI: UploadAPI
|
let uploadAPI: UploadAPI
|
||||||
let ossUploadService: OSSUploadService
|
let ossUploadService: OSSUploadService
|
||||||
@ -54,6 +55,7 @@ final class NetworkServices {
|
|||||||
liveAPI = LiveAPI(client: client)
|
liveAPI = LiveAPI(client: client)
|
||||||
cloudDriveAPI = CloudDriveAPI(client: client)
|
cloudDriveAPI = CloudDriveAPI(client: client)
|
||||||
messageCenterAPI = MessageCenterAPI(client: client)
|
messageCenterAPI = MessageCenterAPI(client: client)
|
||||||
|
pushAPI = PushAPI(client: client)
|
||||||
wildPhotographerReportAPI = WildPhotographerReportAPI(client: client)
|
wildPhotographerReportAPI = WildPhotographerReportAPI(client: client)
|
||||||
uploadAPI = UploadAPI(client: client)
|
uploadAPI = UploadAPI(client: client)
|
||||||
ossUploadService = OSSUploadService(configService: uploadAPI)
|
ossUploadService = OSSUploadService(configService: uploadAPI)
|
||||||
@ -84,6 +86,7 @@ final class NetworkServices {
|
|||||||
liveAPI = LiveAPI(client: apiClient)
|
liveAPI = LiveAPI(client: apiClient)
|
||||||
cloudDriveAPI = CloudDriveAPI(client: apiClient)
|
cloudDriveAPI = CloudDriveAPI(client: apiClient)
|
||||||
messageCenterAPI = MessageCenterAPI(client: apiClient)
|
messageCenterAPI = MessageCenterAPI(client: apiClient)
|
||||||
|
pushAPI = PushAPI(client: apiClient)
|
||||||
wildPhotographerReportAPI = WildPhotographerReportAPI(client: apiClient)
|
wildPhotographerReportAPI = WildPhotographerReportAPI(client: apiClient)
|
||||||
uploadAPI = UploadAPI(client: apiClient)
|
uploadAPI = UploadAPI(client: apiClient)
|
||||||
ossUploadService = OSSUploadService(configService: uploadAPI)
|
ossUploadService = OSSUploadService(configService: uploadAPI)
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
import IQKeyboardManagerSwift
|
import IQKeyboardManagerSwift
|
||||||
import IQKeyboardToolbarManager
|
import IQKeyboardToolbarManager
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
@main
|
@main
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
@ -18,9 +19,36 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
if AppStore.shared.session.privacyAgreementAccepted, !AppStore.shared.session.token.isEmpty {
|
if AppStore.shared.session.privacyAgreementAccepted, !AppStore.shared.session.token.isEmpty {
|
||||||
AMapBootstrap.configureIfNeeded()
|
AMapBootstrap.configureIfNeeded()
|
||||||
}
|
}
|
||||||
|
PushNotificationManager.shared.initializeIfPrivacyAccepted(launchOptions: launchOptions)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// APNs 注册成功后将 Device Token 交给极光。
|
||||||
|
func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
|
||||||
|
) {
|
||||||
|
PushNotificationManager.shared.handleDeviceToken(deviceToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// APNs 注册失败不阻断应用启动,仅交由推送管理器记录。
|
||||||
|
func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didFailToRegisterForRemoteNotificationsWithError error: Error
|
||||||
|
) {
|
||||||
|
PushNotificationManager.shared.handleRegistrationError(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理后台静默通知并完成极光到达统计。
|
||||||
|
func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
|
||||||
|
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
|
||||||
|
) {
|
||||||
|
PushNotificationManager.shared.handleRemoteNotification(userInfo)
|
||||||
|
completionHandler(.newData)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: UISceneSession Lifecycle
|
// MARK: UISceneSession Lifecycle
|
||||||
|
|
||||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||||
|
|||||||
6
suixinkan/Assets.xcassets/Payment/Contents.json
Normal file
6
suixinkan/Assets.xcassets/Payment/Contents.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
21
suixinkan/Assets.xcassets/Payment/payment_nalati_background.imageset/Contents.json
vendored
Normal file
21
suixinkan/Assets.xcassets/Payment/payment_nalati_background.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "payment_nalati_background.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
suixinkan/Assets.xcassets/Payment/payment_nalati_background.imageset/payment_nalati_background.png
vendored
Normal file
BIN
suixinkan/Assets.xcassets/Payment/payment_nalati_background.imageset/payment_nalati_background.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
21
suixinkan/Assets.xcassets/Payment/payment_nalati_logo.imageset/Contents.json
vendored
Normal file
21
suixinkan/Assets.xcassets/Payment/payment_nalati_logo.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "payment_nalati_logo.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
suixinkan/Assets.xcassets/Payment/payment_nalati_logo.imageset/payment_nalati_logo.png
vendored
Normal file
BIN
suixinkan/Assets.xcassets/Payment/payment_nalati_logo.imageset/payment_nalati_logo.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
30
suixinkan/Core/Push/PushAPI.swift
Normal file
30
suixinkan/Core/Push/PushAPI.swift
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// 极光 Registration ID 上报能力,允许推送管理器注入测试替身。
|
||||||
|
@MainActor
|
||||||
|
protocol PushRegistrationServing: AnyObject {
|
||||||
|
/// 将当前设备的极光 Registration ID 绑定到已登录业务账号。
|
||||||
|
func registerJPushID(_ registrationID: String) async throws
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 推送 API,复用 Android 当前使用的 Registration ID 上报接口。
|
||||||
|
@MainActor
|
||||||
|
final class PushAPI: PushRegistrationServing {
|
||||||
|
private let client: APIClient
|
||||||
|
|
||||||
|
/// 使用共享网络客户端创建推送 API。
|
||||||
|
init(client: APIClient) {
|
||||||
|
self.client = client
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 上报极光 Registration ID;后端字段名沿用 `jpush_reg_id`。
|
||||||
|
func registerJPushID(_ registrationID: String) async throws {
|
||||||
|
let _: EmptyPayload = try await client.send(
|
||||||
|
APIRequest(
|
||||||
|
method: .post,
|
||||||
|
path: "/api/app/user/register-jpush-id",
|
||||||
|
queryItems: [URLQueryItem(name: "jpush_reg_id", value: registrationID)]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
86
suixinkan/Core/Push/PushModels.swift
Normal file
86
suixinkan/Core/Push/PushModels.swift
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// 推送点击后可到达的业务页面。
|
||||||
|
enum PushDestination: Sendable, Equatable {
|
||||||
|
case paymentRecord
|
||||||
|
case paymentDetails
|
||||||
|
case messageCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 极光/APNs payload 解析结果,仅提取业务消息类型用于点击路由。
|
||||||
|
struct PushPayload: Sendable, Equatable {
|
||||||
|
private let type: String
|
||||||
|
|
||||||
|
/// 从系统通知 userInfo 提取顶层或推送包装层中的业务消息类型。
|
||||||
|
nonisolated init(userInfo: [AnyHashable: Any]) {
|
||||||
|
type = Self.extractType(from: userInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从可发送的字符串字典创建 payload,仅保留业务消息类型。
|
||||||
|
nonisolated init(values: [String: String]) {
|
||||||
|
type = Self.normalizedType(values["type"])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 可发送的规范化字段快照。
|
||||||
|
nonisolated var normalizedValues: [String: String] {
|
||||||
|
type.isEmpty ? [:] : ["type": type]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 仅根据后端业务消息类型解析目标页面。
|
||||||
|
nonisolated var destination: PushDestination {
|
||||||
|
switch type {
|
||||||
|
case "1":
|
||||||
|
return .paymentRecord
|
||||||
|
case "6":
|
||||||
|
return .paymentDetails
|
||||||
|
default:
|
||||||
|
return .messageCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private nonisolated static func extractType(
|
||||||
|
from dictionary: [AnyHashable: Any],
|
||||||
|
depth: Int = 0
|
||||||
|
) -> String {
|
||||||
|
guard depth <= 4 else { return "" }
|
||||||
|
let directType = normalizedType(dictionary["type"])
|
||||||
|
guard directType.isEmpty else { return directType }
|
||||||
|
|
||||||
|
for key in nestedTypeContainerKeys {
|
||||||
|
guard let value = dictionary[key] else { continue }
|
||||||
|
if let nested = value as? [AnyHashable: Any] {
|
||||||
|
let nestedType = extractType(from: nested, depth: depth + 1)
|
||||||
|
if !nestedType.isEmpty { return nestedType }
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let text = value as? String,
|
||||||
|
let data = text.data(using: .utf8),
|
||||||
|
let object = try? JSONSerialization.jsonObject(with: data),
|
||||||
|
let nested = object as? [String: Any]
|
||||||
|
else { continue }
|
||||||
|
|
||||||
|
let bridged = Dictionary(uniqueKeysWithValues: nested.map {
|
||||||
|
(AnyHashable($0.key), $0.value)
|
||||||
|
})
|
||||||
|
let nestedType = extractType(from: bridged, depth: depth + 1)
|
||||||
|
if !nestedType.isEmpty { return nestedType }
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private nonisolated static func normalizedType(_ value: Any?) -> String {
|
||||||
|
switch value {
|
||||||
|
case let string as String:
|
||||||
|
return string.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
case let number as NSNumber:
|
||||||
|
return number.stringValue
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private nonisolated static let nestedTypeContainerKeys = [
|
||||||
|
"extras", "extra", "JMessageExtra", "n_extras",
|
||||||
|
]
|
||||||
|
}
|
||||||
422
suixinkan/Core/Push/PushNotificationManager.swift
Normal file
422
suixinkan/Core/Push/PushNotificationManager.swift
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
import UIKit
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
|
/// App 桌面图标角标设置能力,便于业务同步未读消息数量并进行测试替换。
|
||||||
|
@MainActor
|
||||||
|
protocol ApplicationIconBadgeSetting: AnyObject {
|
||||||
|
/// 将桌面图标角标更新为指定非负数量。
|
||||||
|
func setBadgeCount(_ count: Int) async
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用 UserNotifications 更新 App 桌面图标角标的系统实现。
|
||||||
|
@MainActor
|
||||||
|
final class SystemApplicationIconBadgeSetter: ApplicationIconBadgeSetting {
|
||||||
|
/// 全局角标设置器。
|
||||||
|
static let shared = SystemApplicationIconBadgeSetter()
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
func setBadgeCount(_ count: Int) async {
|
||||||
|
do {
|
||||||
|
try await UNUserNotificationCenter.current().setBadgeCount(max(count, 0))
|
||||||
|
} catch {
|
||||||
|
#if DEBUG
|
||||||
|
print("Update application icon badge failed: \(error.localizedDescription)")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 极光 SDK 的最小能力边界,便于推送生命周期逻辑使用测试替身。
|
||||||
|
@MainActor
|
||||||
|
protocol PushSDKProviding: AnyObject {
|
||||||
|
/// 初始化极光长连接与 APNs 代理服务。
|
||||||
|
func initialize(launchOptions: [UIApplication.LaunchOptionsKey: Any]?, isProduction: Bool)
|
||||||
|
|
||||||
|
/// 请求系统通知权限并注册极光通知代理。
|
||||||
|
func requestAuthorization(delegate: JPUSHRegisterDelegate)
|
||||||
|
|
||||||
|
/// 将 APNs Device Token 注册给极光。
|
||||||
|
func registerDeviceToken(_ deviceToken: Data)
|
||||||
|
|
||||||
|
/// 获取极光 Registration ID。
|
||||||
|
func fetchRegistrationID(completion: @escaping (Int32, String?) -> Void)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 跨越极光 `@Sendable` 回调边界的只读闭包容器。
|
||||||
|
private final class RegistrationIDCompletionBox: @unchecked Sendable {
|
||||||
|
let completion: (Int32, String?) -> Void
|
||||||
|
|
||||||
|
init(completion: @escaping (Int32, String?) -> Void) {
|
||||||
|
self.completion = completion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生产环境极光 SDK 适配器,集中保存 AppKey 和渠道配置。
|
||||||
|
@MainActor
|
||||||
|
final class JPushSDKAdapter: PushSDKProviding {
|
||||||
|
private static let appKey = "194a633c2a6b79c2f1033c40"
|
||||||
|
private static let channel = "App Store"
|
||||||
|
|
||||||
|
/// 初始化极光 SDK;Debug 输出详细日志,Release 只保留告警和错误。
|
||||||
|
func initialize(launchOptions: [UIApplication.LaunchOptionsKey: Any]?, isProduction: Bool) {
|
||||||
|
JPUSHService.setup(
|
||||||
|
withOption: launchOptions,
|
||||||
|
appKey: Self.appKey,
|
||||||
|
channel: Self.channel,
|
||||||
|
apsForProduction: isProduction
|
||||||
|
)
|
||||||
|
#if DEBUG
|
||||||
|
JPUSHService.setDebugMode()
|
||||||
|
#else
|
||||||
|
JPUSHService.setLogOFF()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 请求 alert、badge、sound 三类通知权限。
|
||||||
|
func requestAuthorization(delegate: JPUSHRegisterDelegate) {
|
||||||
|
let entity = JPUSHRegisterEntity()
|
||||||
|
entity.types = 1 | 2 | 4
|
||||||
|
JPUSHService.register(forRemoteNotificationConfig: entity, delegate: delegate)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将 APNs Device Token 交给极光。
|
||||||
|
func registerDeviceToken(_ deviceToken: Data) {
|
||||||
|
JPUSHService.registerDeviceToken(deviceToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 异步获取当前设备 Registration ID。
|
||||||
|
func fetchRegistrationID(completion: @escaping (Int32, String?) -> Void) {
|
||||||
|
let box = RegistrationIDCompletionBox(completion: completion)
|
||||||
|
JPUSHService.registrationIDCompletionHandler { resultCode, registrationID in
|
||||||
|
box.completion(resultCode, registrationID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 推送生命周期管理器,负责隐私门禁、权限、Registration ID 绑定及通知点击分发。
|
||||||
|
@MainActor
|
||||||
|
final class PushNotificationManager: NSObject {
|
||||||
|
/// 全局推送管理器。
|
||||||
|
static let shared = PushNotificationManager()
|
||||||
|
|
||||||
|
/// 推送相关的本地持久化键。
|
||||||
|
private enum Key {
|
||||||
|
static let registrationID = "key_jpush_registration_id"
|
||||||
|
static let uploadedRegistrationIDSuffix = "jpush_uploaded_registration_id"
|
||||||
|
}
|
||||||
|
|
||||||
|
private let sdk: any PushSDKProviding
|
||||||
|
private let api: any PushRegistrationServing
|
||||||
|
private let appStore: AppStore
|
||||||
|
private let defaults: UserDefaults
|
||||||
|
private let router: any PushRouting
|
||||||
|
private let applicationIconBadgeSetter: any ApplicationIconBadgeSetting
|
||||||
|
|
||||||
|
private var isInitialized = false
|
||||||
|
private var didRequestAuthorization = false
|
||||||
|
private var uploadTask: Task<Void, Never>?
|
||||||
|
private var queuedForcedUpload = false
|
||||||
|
private var isFetchingRegistrationID = false
|
||||||
|
private var queuedForcedFetch = false
|
||||||
|
|
||||||
|
/// 创建推送管理器;生产环境使用共享依赖,测试可注入替身。
|
||||||
|
init(
|
||||||
|
sdk: (any PushSDKProviding)? = nil,
|
||||||
|
api: (any PushRegistrationServing)? = nil,
|
||||||
|
appStore: AppStore = .shared,
|
||||||
|
defaults: UserDefaults = .standard,
|
||||||
|
router: (any PushRouting)? = nil,
|
||||||
|
applicationIconBadgeSetter: (any ApplicationIconBadgeSetting)? = nil
|
||||||
|
) {
|
||||||
|
self.sdk = sdk ?? JPushSDKAdapter()
|
||||||
|
self.api = api ?? NetworkServices.shared.pushAPI
|
||||||
|
self.appStore = appStore
|
||||||
|
self.defaults = defaults
|
||||||
|
self.router = router ?? PushRouteCoordinator.shared
|
||||||
|
self.applicationIconBadgeSetter = applicationIconBadgeSetter ?? SystemApplicationIconBadgeSetter.shared
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 绑定当前 Scene 的主窗口。
|
||||||
|
func attach(window: UIWindow) {
|
||||||
|
router.attach(window: window)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 仅在用户已同意隐私协议后初始化极光 SDK。
|
||||||
|
func initializeIfPrivacyAccepted(
|
||||||
|
launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||||
|
) {
|
||||||
|
guard appStore.session.privacyAgreementAccepted, !isInitialized else { return }
|
||||||
|
isInitialized = true
|
||||||
|
sdk.initialize(launchOptions: launchOptions, isProduction: Self.isProductionBuild)
|
||||||
|
observeJPushNetworkLogin()
|
||||||
|
refreshRegistrationID(forceUpload: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 登录成功后请求通知权限,并强制绑定当前账号。
|
||||||
|
func handleLoginCompleted() {
|
||||||
|
initializeIfPrivacyAccepted()
|
||||||
|
guard isInitialized, appStore.session.isLoggedIn else { return }
|
||||||
|
if !didRequestAuthorization {
|
||||||
|
didRequestAuthorization = true
|
||||||
|
sdk.requestAuthorization(delegate: self)
|
||||||
|
}
|
||||||
|
bindCurrentAccount()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 账号切换后把同一设备重新绑定到新的业务账号。
|
||||||
|
func handleAccountSwitched() {
|
||||||
|
guard appStore.session.isLoggedIn else { return }
|
||||||
|
bindCurrentAccount()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 退出登录时停止账号上报并清除未完成的业务路由。
|
||||||
|
func handleLogout() {
|
||||||
|
uploadTask?.cancel()
|
||||||
|
uploadTask = nil
|
||||||
|
queuedForcedUpload = false
|
||||||
|
router.resetPendingRoute()
|
||||||
|
Task { await updateApplicationIconBadgeCount(0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将桌面 App Icon 角标更新为最新未读消息数量。
|
||||||
|
func updateApplicationIconBadgeCount(_ count: Int) async {
|
||||||
|
await applicationIconBadgeSetter.setBadgeCount(max(count, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// App 回到前台时补偿失败或尚未完成的 Registration ID 上报。
|
||||||
|
func retryPendingRegistrationUpload() {
|
||||||
|
guard appStore.session.isLoggedIn else { return }
|
||||||
|
uploadCachedRegistrationID(force: false)
|
||||||
|
refreshRegistrationID(forceUpload: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 登录根页面建立后继续执行通知点击暂存的路由。
|
||||||
|
func routePendingNotificationIfPossible() {
|
||||||
|
router.routePendingIfPossible()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// APNs 注册成功后把 Device Token 注册给极光。
|
||||||
|
func handleDeviceToken(_ deviceToken: Data) {
|
||||||
|
guard isInitialized else { return }
|
||||||
|
sdk.registerDeviceToken(deviceToken)
|
||||||
|
refreshRegistrationID(forceUpload: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// APNs 注册失败只记录调试信息,不阻断登录和主业务。
|
||||||
|
func handleRegistrationError(_ error: Error) {
|
||||||
|
#if DEBUG
|
||||||
|
print("JPush APNs registration failed: \(error.localizedDescription)")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理冷启动或运行中通知点击。
|
||||||
|
func handleNotificationTap(
|
||||||
|
payload: PushPayload,
|
||||||
|
requestIdentifier: String
|
||||||
|
) {
|
||||||
|
router.handle(
|
||||||
|
destination: payload.destination,
|
||||||
|
requestIdentifier: requestIdentifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理 Scene 冷启动携带的系统通知响应,并向极光上报点击。
|
||||||
|
func handleNotificationResponse(_ response: UNNotificationResponse) {
|
||||||
|
let userInfo = response.notification.request.content.userInfo
|
||||||
|
Self.logReceivedPush(userInfo, source: "tap(cold-start)")
|
||||||
|
JPUSHService.handleRemoteNotification(userInfo)
|
||||||
|
handleNotificationTap(
|
||||||
|
payload: PushPayload(userInfo: userInfo),
|
||||||
|
requestIdentifier: response.notification.request.identifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理后台静默通知并向极光上报到达,不触发页面跳转。
|
||||||
|
func handleRemoteNotification(_ userInfo: [AnyHashable: Any]) {
|
||||||
|
Self.logReceivedPush(userInfo, source: "background")
|
||||||
|
JPUSHService.handleRemoteNotification(userInfo)
|
||||||
|
NotificationCenter.default.post(name: NotificationName.unreadMessageCountDidChange, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将收到的推送 payload 打印到控制台,便于联调排查。
|
||||||
|
private nonisolated static func logReceivedPush(
|
||||||
|
_ userInfo: [AnyHashable: Any],
|
||||||
|
source: String
|
||||||
|
) {
|
||||||
|
#if DEBUG
|
||||||
|
let payloadText: String
|
||||||
|
let normalized = Dictionary(uniqueKeysWithValues: userInfo.compactMap { key, value in
|
||||||
|
(key as? String).map { ($0, value) }
|
||||||
|
})
|
||||||
|
if JSONSerialization.isValidJSONObject(normalized),
|
||||||
|
let data = try? JSONSerialization.data(
|
||||||
|
withJSONObject: normalized,
|
||||||
|
options: [.prettyPrinted, .sortedKeys]
|
||||||
|
),
|
||||||
|
let text = String(data: data, encoding: .utf8) {
|
||||||
|
payloadText = text
|
||||||
|
} else {
|
||||||
|
payloadText = String(describing: userInfo)
|
||||||
|
}
|
||||||
|
print("[Push] received (\(source)):\n\(payloadText)")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private func observeJPushNetworkLogin() {
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(handleJPushNetworkLogin),
|
||||||
|
name: .jpfNetworkDidLogin,
|
||||||
|
object: nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handleJPushNetworkLogin() {
|
||||||
|
refreshRegistrationID(forceUpload: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshRegistrationID(forceUpload: Bool) {
|
||||||
|
guard isInitialized else { return }
|
||||||
|
queuedForcedFetch = queuedForcedFetch || forceUpload
|
||||||
|
guard !isFetchingRegistrationID else { return }
|
||||||
|
isFetchingRegistrationID = true
|
||||||
|
sdk.fetchRegistrationID { [weak self] resultCode, registrationID in
|
||||||
|
Task { @MainActor in
|
||||||
|
guard let self else { return }
|
||||||
|
let shouldForceUpload = self.queuedForcedFetch
|
||||||
|
self.queuedForcedFetch = false
|
||||||
|
self.isFetchingRegistrationID = false
|
||||||
|
guard resultCode == 0 else { return }
|
||||||
|
let normalized = registrationID?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
|
guard !normalized.isEmpty else { return }
|
||||||
|
self.defaults.set(normalized, forKey: Key.registrationID)
|
||||||
|
self.upload(registrationID: normalized, force: shouldForceUpload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func uploadCachedRegistrationID(force: Bool) {
|
||||||
|
let registrationID = cachedRegistrationID
|
||||||
|
guard !registrationID.isEmpty else { return }
|
||||||
|
upload(registrationID: registrationID, force: force)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bindCurrentAccount() {
|
||||||
|
if cachedRegistrationID.isEmpty {
|
||||||
|
refreshRegistrationID(forceUpload: true)
|
||||||
|
} else {
|
||||||
|
uploadCachedRegistrationID(force: true)
|
||||||
|
refreshRegistrationID(forceUpload: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cachedRegistrationID: String {
|
||||||
|
defaults.string(forKey: Key.registrationID)?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private func upload(registrationID: String, force: Bool) {
|
||||||
|
guard appStore.session.isLoggedIn,
|
||||||
|
let uploadedKey = appStore.session.accountScopedKey(Key.uploadedRegistrationIDSuffix)
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
if !force, defaults.string(forKey: uploadedKey) == registrationID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if uploadTask != nil {
|
||||||
|
queuedForcedUpload = queuedForcedUpload || force
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let accountScope = appStore.session.accountCachePrefix
|
||||||
|
uploadTask = Task { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
var succeeded = false
|
||||||
|
do {
|
||||||
|
try await self.api.registerJPushID(registrationID)
|
||||||
|
if self.appStore.session.accountCachePrefix == accountScope {
|
||||||
|
self.defaults.set(registrationID, forKey: uploadedKey)
|
||||||
|
}
|
||||||
|
succeeded = true
|
||||||
|
} catch is CancellationError {
|
||||||
|
// 退出登录时取消,不视为失败重试。
|
||||||
|
} catch {
|
||||||
|
#if DEBUG
|
||||||
|
print("JPush Registration ID upload failed: \(error.localizedDescription)")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
self.uploadTask = nil
|
||||||
|
let shouldForceAgain = self.queuedForcedUpload
|
||||||
|
self.queuedForcedUpload = false
|
||||||
|
if succeeded, shouldForceAgain {
|
||||||
|
self.uploadCachedRegistrationID(force: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private nonisolated static var isProductionBuild: Bool {
|
||||||
|
#if DEBUG
|
||||||
|
false
|
||||||
|
#else
|
||||||
|
true
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PushNotificationManager: JPUSHRegisterDelegate {
|
||||||
|
/// 前台通知只展示系统 UI,不自动执行业务路由。
|
||||||
|
nonisolated func jpushNotificationCenter(
|
||||||
|
_ center: UNUserNotificationCenter,
|
||||||
|
willPresent notification: UNNotification,
|
||||||
|
withCompletionHandler completionHandler: @escaping (Int) -> Void
|
||||||
|
) {
|
||||||
|
let userInfo = notification.request.content.userInfo
|
||||||
|
Self.logReceivedPush(userInfo, source: "foreground")
|
||||||
|
JPUSHService.handleRemoteNotification(userInfo)
|
||||||
|
Task { @MainActor in
|
||||||
|
NotificationCenter.default.post(name: NotificationName.unreadMessageCountDidChange, object: nil)
|
||||||
|
}
|
||||||
|
let options: UNNotificationPresentationOptions = [.banner, .list, .sound, .badge]
|
||||||
|
completionHandler(Int(options.rawValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 用户点击通知后上报极光统计,并在主 Actor 执行业务路由。
|
||||||
|
nonisolated func jpushNotificationCenter(
|
||||||
|
_ center: UNUserNotificationCenter,
|
||||||
|
didReceive response: UNNotificationResponse,
|
||||||
|
withCompletionHandler completionHandler: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
let userInfo = response.notification.request.content.userInfo
|
||||||
|
Self.logReceivedPush(userInfo, source: "tap")
|
||||||
|
JPUSHService.handleRemoteNotification(userInfo)
|
||||||
|
let payload = PushPayload(userInfo: userInfo)
|
||||||
|
let requestIdentifier = response.notification.request.identifier
|
||||||
|
Task { @MainActor [weak self] in
|
||||||
|
self?.handleNotificationTap(
|
||||||
|
payload: payload,
|
||||||
|
requestIdentifier: requestIdentifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
completionHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 系统通知设置入口回调,首期无需额外应用内页面。
|
||||||
|
nonisolated func jpushNotificationCenter(
|
||||||
|
_ center: UNUserNotificationCenter,
|
||||||
|
openSettingsFor notification: UNNotification
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/// 极光通知授权状态回调,权限变化由系统设置管理。
|
||||||
|
nonisolated func jpushNotificationAuthorization(
|
||||||
|
_ status: JPAuthorizationStatus,
|
||||||
|
withInfo info: [AnyHashable: Any]?
|
||||||
|
) {}
|
||||||
|
}
|
||||||
98
suixinkan/Core/Push/PushRouteCoordinator.swift
Normal file
98
suixinkan/Core/Push/PushRouteCoordinator.swift
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 推送点击路由能力,允许生命周期管理器与 UIKit 导航解耦并支持测试替身。
|
||||||
|
@MainActor
|
||||||
|
protocol PushRouting: AnyObject {
|
||||||
|
/// 绑定当前 Scene 的主窗口。
|
||||||
|
func attach(window: UIWindow)
|
||||||
|
|
||||||
|
/// 处理一次通知点击;未登录时暂存到登录成功后继续。
|
||||||
|
func handle(destination: PushDestination, requestIdentifier: String)
|
||||||
|
|
||||||
|
/// 主界面建立后执行尚未完成的通知路由。
|
||||||
|
func routePendingIfPossible()
|
||||||
|
|
||||||
|
/// 清除退出登录前遗留的待处理路由。
|
||||||
|
func resetPendingRoute()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 集中式推送路由器,负责冷启动去重、登录等待和跨 Tab 导航。
|
||||||
|
@MainActor
|
||||||
|
final class PushRouteCoordinator: PushRouting {
|
||||||
|
/// 全局推送路由器。
|
||||||
|
static let shared = PushRouteCoordinator()
|
||||||
|
|
||||||
|
/// 等待登录或主界面建立后执行的一次性推送路由。
|
||||||
|
private struct PendingRoute {
|
||||||
|
let destination: PushDestination
|
||||||
|
let requestIdentifier: String
|
||||||
|
}
|
||||||
|
|
||||||
|
private weak var window: UIWindow?
|
||||||
|
private var pendingRoute: PendingRoute?
|
||||||
|
private var handledRequestIdentifiers: [String] = []
|
||||||
|
private let appStore: AppStore
|
||||||
|
|
||||||
|
/// 创建路由器并注入应用状态。
|
||||||
|
init(appStore: AppStore = .shared) {
|
||||||
|
self.appStore = appStore
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 绑定当前 Scene 的主窗口。
|
||||||
|
func attach(window: UIWindow) {
|
||||||
|
self.window = window
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理通知点击;相同 request identifier 只执行一次。
|
||||||
|
func handle(destination: PushDestination, requestIdentifier: String) {
|
||||||
|
let identifier = requestIdentifier.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if !identifier.isEmpty, handledRequestIdentifiers.contains(identifier) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
markHandled(identifier)
|
||||||
|
|
||||||
|
let route = PendingRoute(destination: destination, requestIdentifier: identifier)
|
||||||
|
guard appStore.session.isLoggedIn, mainTabController != nil else {
|
||||||
|
pendingRoute = route
|
||||||
|
return
|
||||||
|
}
|
||||||
|
perform(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 登录完成、根页面切换后继续执行最后一次待处理路由。
|
||||||
|
func routePendingIfPossible() {
|
||||||
|
guard appStore.session.isLoggedIn,
|
||||||
|
mainTabController != nil,
|
||||||
|
let route = pendingRoute
|
||||||
|
else { return }
|
||||||
|
pendingRoute = nil
|
||||||
|
perform(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 用户退出登录时丢弃之前账号尚未执行的跳转。
|
||||||
|
func resetPendingRoute() {
|
||||||
|
pendingRoute = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mainTabController: MainTabBarController? {
|
||||||
|
window?.rootViewController as? MainTabBarController
|
||||||
|
}
|
||||||
|
|
||||||
|
private func perform(_ route: PendingRoute) {
|
||||||
|
guard let mainTabController else {
|
||||||
|
pendingRoute = route
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mainTabController.dismiss(animated: false)
|
||||||
|
mainTabController.openPushDestination(route.destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func markHandled(_ identifier: String) {
|
||||||
|
guard !identifier.isEmpty else { return }
|
||||||
|
handledRequestIdentifiers.append(identifier)
|
||||||
|
if handledRequestIdentifiers.count > 50 {
|
||||||
|
handledRequestIdentifiers.removeFirst(handledRequestIdentifiers.count - 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,10 +9,40 @@ import Foundation
|
|||||||
struct RolePermissionResponse: Codable, Equatable, Sendable {
|
struct RolePermissionResponse: Codable, Equatable, Sendable {
|
||||||
let role: RoleInfo
|
let role: RoleInfo
|
||||||
let scenic: [ScenicInfo]
|
let scenic: [ScenicInfo]
|
||||||
|
let store: [RolePermissionStoreInfo]
|
||||||
|
|
||||||
init(role: RoleInfo, scenic: [ScenicInfo] = []) {
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case role
|
||||||
|
case scenic
|
||||||
|
case store
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
role: RoleInfo,
|
||||||
|
scenic: [ScenicInfo] = [],
|
||||||
|
store: [RolePermissionStoreInfo] = []
|
||||||
|
) {
|
||||||
self.role = role
|
self.role = role
|
||||||
self.scenic = scenic
|
self.scenic = scenic
|
||||||
|
self.store = store
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
role = try container.decode(RoleInfo.self, forKey: .role)
|
||||||
|
scenic = try container.decodeIfPresent([ScenicInfo].self, forKey: .scenic) ?? []
|
||||||
|
store = try container.decodeIfPresent([RolePermissionStoreInfo].self, forKey: .store) ?? []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 当前角色账号可使用的店铺摘要,来源于 `role-permission` 的 `store` 字段。
|
||||||
|
struct RolePermissionStoreInfo: Codable, Equatable, Sendable {
|
||||||
|
let id: Int
|
||||||
|
let name: String
|
||||||
|
|
||||||
|
init(id: Int = 0, name: String = "") {
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,18 +17,23 @@ final class HomeCommonMenuStore {
|
|||||||
/// 读取已保存的常用 URI;无记录时返回空数组。
|
/// 读取已保存的常用 URI;无记录时返回空数组。
|
||||||
func savedCommonURIs(accountScope: String, roleCode: String) -> [String] {
|
func savedCommonURIs(accountScope: String, roleCode: String) -> [String] {
|
||||||
guard let key = storageKey(accountScope: accountScope, roleCode: roleCode) else { return [] }
|
guard let key = storageKey(accountScope: accountScope, roleCode: roleCode) else { return [] }
|
||||||
return defaults.stringArray(forKey: key) ?? []
|
let saved = defaults.stringArray(forKey: key) ?? []
|
||||||
|
let sanitized = saved.filter(HomeMenuCatalog.isCustomizable(uri:))
|
||||||
|
if sanitized != saved {
|
||||||
|
defaults.set(sanitized, forKey: key)
|
||||||
|
}
|
||||||
|
return sanitized
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 保存常用 URI 列表。
|
/// 保存常用 URI 列表。
|
||||||
func saveCommonURIs(_ uris: [String], accountScope: String, roleCode: String) {
|
func saveCommonURIs(_ uris: [String], accountScope: String, roleCode: String) {
|
||||||
guard let key = storageKey(accountScope: accountScope, roleCode: roleCode) else { return }
|
guard let key = storageKey(accountScope: accountScope, roleCode: roleCode) else { return }
|
||||||
defaults.set(uris, forKey: key)
|
defaults.set(uris.filter(HomeMenuCatalog.isCustomizable(uri:)), forKey: key)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 根据权限生成默认常用 URI(最多 4 个)。
|
/// 根据权限生成默认常用 URI(最多 4 个)。
|
||||||
static func defaultCommonURIs(from permissions: [HomePermissionItem]) -> [String] {
|
static func defaultCommonURIs(from permissions: [HomePermissionItem]) -> [String] {
|
||||||
let uris = permissions.map(\.uri)
|
let uris = permissions.map(\.uri).filter(HomeMenuCatalog.isCustomizable(uri:))
|
||||||
return uris.count > 4 ? Array(uris.prefix(4)) : uris
|
return uris.count > 4 ? Array(uris.prefix(4)) : uris
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,8 +56,9 @@ final class HomeCommonMenuStore {
|
|||||||
commonURIs: [String]
|
commonURIs: [String]
|
||||||
) -> (common: [HomeMenuItem], more: [HomeMenuItem]) {
|
) -> (common: [HomeMenuItem], more: [HomeMenuItem]) {
|
||||||
let commonSet = Set(commonURIs)
|
let commonSet = Set(commonURIs)
|
||||||
let common = allMenus.filter { commonSet.contains($0.uri) }
|
let customizableMenus = allMenus.filter { HomeMenuCatalog.isCustomizable(uri: $0.uri) }
|
||||||
let more = allMenus.filter { !commonSet.contains($0.uri) }
|
let common = customizableMenus.filter { commonSet.contains($0.uri) }
|
||||||
|
let more = customizableMenus.filter { !commonSet.contains($0.uri) }
|
||||||
return (common, more)
|
return (common, more)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import Foundation
|
|||||||
/// 首页菜单静态 catalog,与服务端 permission URI 取交集后展示。
|
/// 首页菜单静态 catalog,与服务端 permission URI 取交集后展示。
|
||||||
enum HomeMenuCatalog {
|
enum HomeMenuCatalog {
|
||||||
|
|
||||||
|
private static let fixedHomeEntryURIs: Set<String> = ["message_center"]
|
||||||
|
|
||||||
/// 全部已知菜单项,对齐 Android `Constants.menuList`。
|
/// 全部已知菜单项,对齐 Android `Constants.menuList`。
|
||||||
static let allItems: [HomeMenuItem] = [
|
static let allItems: [HomeMenuItem] = [
|
||||||
HomeMenuItem(uri: "space_settings", title: "空间设置", iconName: "home_menu_space"),
|
HomeMenuItem(uri: "space_settings", title: "空间设置", iconName: "home_menu_space"),
|
||||||
@ -45,6 +47,7 @@ enum HomeMenuCatalog {
|
|||||||
/// 将服务端权限映射为可展示菜单,保持服务端顺序。
|
/// 将服务端权限映射为可展示菜单,保持服务端顺序。
|
||||||
static func visibleMenus(from permissions: [HomePermissionItem]) -> [HomeMenuItem] {
|
static func visibleMenus(from permissions: [HomePermissionItem]) -> [HomeMenuItem] {
|
||||||
permissions.compactMap { permission in
|
permissions.compactMap { permission in
|
||||||
|
guard isCustomizable(uri: permission.uri) else { return nil }
|
||||||
guard let catalog = allItems.first(where: { $0.uri == permission.uri }) else { return nil }
|
guard let catalog = allItems.first(where: { $0.uri == permission.uri }) else { return nil }
|
||||||
return HomeMenuItem(
|
return HomeMenuItem(
|
||||||
uri: catalog.uri,
|
uri: catalog.uri,
|
||||||
@ -59,4 +62,9 @@ enum HomeMenuCatalog {
|
|||||||
static func item(for uri: String) -> HomeMenuItem? {
|
static func item(for uri: String) -> HomeMenuItem? {
|
||||||
allItems.first { $0.uri == uri }
|
allItems.first { $0.uri == uri }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 判断入口是否允许出现在首页常用应用及其自定义页面中。
|
||||||
|
static func isCustomizable(uri: String) -> Bool {
|
||||||
|
!fixedHomeEntryURIs.contains(uri)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,6 +48,7 @@ final class AllFunctionsViewModel {
|
|||||||
|
|
||||||
/// 将菜单加入常用应用。
|
/// 将菜单加入常用应用。
|
||||||
func addToCommon(_ menu: HomeMenuItem) {
|
func addToCommon(_ menu: HomeMenuItem) {
|
||||||
|
guard HomeMenuCatalog.isCustomizable(uri: menu.uri) else { return }
|
||||||
guard !commonMenus.contains(where: { $0.uri == menu.uri }) else { return }
|
guard !commonMenus.contains(where: { $0.uri == menu.uri }) else { return }
|
||||||
commonMenus.append(menu)
|
commonMenus.append(menu)
|
||||||
moreMenus.removeAll { $0.uri == menu.uri }
|
moreMenus.removeAll { $0.uri == menu.uri }
|
||||||
|
|||||||
@ -25,6 +25,7 @@ final class HomeViewModel {
|
|||||||
private(set) var reportTimeText = ""
|
private(set) var reportTimeText = ""
|
||||||
private(set) var isLocationReporting = false
|
private(set) var isLocationReporting = false
|
||||||
private(set) var needsPermissionReload = false
|
private(set) var needsPermissionReload = false
|
||||||
|
private(set) var unreadMessageCount = 0
|
||||||
|
|
||||||
var onStateChange: (() -> Void)?
|
var onStateChange: (() -> Void)?
|
||||||
var onShowMessage: ((String) -> Void)?
|
var onShowMessage: ((String) -> Void)?
|
||||||
@ -65,6 +66,7 @@ final class HomeViewModel {
|
|||||||
var isOnline: Bool { locationStateStore.isOnline }
|
var isOnline: Bool { locationStateStore.isOnline }
|
||||||
var countdownDisplayText: String { locationStateStore.countdownDisplayText }
|
var countdownDisplayText: String { locationStateStore.countdownDisplayText }
|
||||||
var reminderMinutes: Int { locationStateStore.reminderMinutes }
|
var reminderMinutes: Int { locationStateStore.reminderMinutes }
|
||||||
|
var hasUnreadMessages: Bool { unreadMessageCount > 0 }
|
||||||
|
|
||||||
/// 首次进入首页时加载权限并恢复在线状态。
|
/// 首次进入首页时加载权限并恢复在线状态。
|
||||||
func initialize(api: HomeAPI) async {
|
func initialize(api: HomeAPI) async {
|
||||||
@ -76,6 +78,28 @@ final class HomeViewModel {
|
|||||||
/// 账号切换后标记需要重新拉取权限。
|
/// 账号切换后标记需要重新拉取权限。
|
||||||
func markNeedsPermissionReload() {
|
func markNeedsPermissionReload() {
|
||||||
needsPermissionReload = true
|
needsPermissionReload = true
|
||||||
|
unreadMessageCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查询未读消息总数,成功时更新首页状态并返回 `true`。
|
||||||
|
func refreshUnreadMessageStatus(api: any MessageCenterServing) async -> Bool {
|
||||||
|
do {
|
||||||
|
let response = try await api.unreadCount()
|
||||||
|
unreadMessageCount = response.unreadCount
|
||||||
|
notifyStateChange()
|
||||||
|
return true
|
||||||
|
} catch is CancellationError {
|
||||||
|
return false
|
||||||
|
} catch {
|
||||||
|
// 查询失败时保留上一次状态,避免网络抖动导致红点错误消失。
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用已知的最新未读消息数量立即刷新首页状态。
|
||||||
|
func updateUnreadMessageCount(_ count: Int) {
|
||||||
|
unreadMessageCount = max(count, 0)
|
||||||
|
notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 查询待审核记录并决定进入申请页或审核状态页。
|
/// 查询待审核记录并决定进入申请页或审核状态页。
|
||||||
|
|||||||
@ -11,8 +11,14 @@ protocol MessageCenterServing {
|
|||||||
/// 拉取消息列表。
|
/// 拉取消息列表。
|
||||||
func list(lastId: Int, limit: Int, unread: Int) async throws -> MessageListResponse
|
func list(lastId: Int, limit: Int, unread: Int) async throws -> MessageListResponse
|
||||||
|
|
||||||
/// 标记消息已读。
|
/// 获取当前账号未读消息总数。
|
||||||
func markAsRead(messageId: Int) async throws
|
func unreadCount() async throws -> MessageUnreadCountResponse
|
||||||
|
|
||||||
|
/// 标记消息已读并返回最新未读数量。
|
||||||
|
func markAsRead(messageId: Int) async throws -> MessageUnreadCountResponse
|
||||||
|
|
||||||
|
/// 将当前账号全部消息标记已读。
|
||||||
|
func markAllAsRead() async throws -> MessageReadAllResponse
|
||||||
|
|
||||||
/// 删除消息。
|
/// 删除消息。
|
||||||
func delete(messageId: Int) async throws
|
func delete(messageId: Int) async throws
|
||||||
@ -43,9 +49,16 @@ final class MessageCenterAPI: MessageCenterServing {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GET /api/app/msg/unread-count
|
||||||
|
func unreadCount() async throws -> MessageUnreadCountResponse {
|
||||||
|
try await client.send(
|
||||||
|
APIRequest(method: .get, path: "/api/app/msg/unread-count")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// POST /api/app/msg/read
|
/// POST /api/app/msg/read
|
||||||
func markAsRead(messageId: Int) async throws {
|
func markAsRead(messageId: Int) async throws -> MessageUnreadCountResponse {
|
||||||
let _: EmptyPayload = try await client.send(
|
try await client.send(
|
||||||
APIRequest(
|
APIRequest(
|
||||||
method: .post,
|
method: .post,
|
||||||
path: "/api/app/msg/read",
|
path: "/api/app/msg/read",
|
||||||
@ -54,6 +67,13 @@ final class MessageCenterAPI: MessageCenterServing {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// POST /api/app/msg/read-all
|
||||||
|
func markAllAsRead() async throws -> MessageReadAllResponse {
|
||||||
|
try await client.send(
|
||||||
|
APIRequest(method: .post, path: "/api/app/msg/read-all")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// POST /api/app/msg/delete
|
/// POST /api/app/msg/delete
|
||||||
func delete(messageId: Int) async throws {
|
func delete(messageId: Int) async throws {
|
||||||
let _: EmptyPayload = try await client.send(
|
let _: EmptyPayload = try await client.send(
|
||||||
|
|||||||
@ -32,6 +32,46 @@ enum MessageJSONValue: Decodable, Hashable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 消息未读数量响应,用于同步首页红点和桌面图标角标。
|
||||||
|
struct MessageUnreadCountResponse: Decodable, Equatable, Sendable {
|
||||||
|
let unreadCount: Int
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case unreadCount = "unread_count"
|
||||||
|
}
|
||||||
|
|
||||||
|
init(unreadCount: Int = 0) {
|
||||||
|
self.unreadCount = max(unreadCount, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
unreadCount = max(try container.decodeLossyInt(forKey: .unreadCount) ?? 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 全部标记已读响应,包含本次更新数量和操作后的未读数量。
|
||||||
|
struct MessageReadAllResponse: Decodable, Equatable, Sendable {
|
||||||
|
let updatedCount: Int
|
||||||
|
let unreadCount: Int
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case updatedCount = "updated_count"
|
||||||
|
case unreadCount = "unread_count"
|
||||||
|
}
|
||||||
|
|
||||||
|
init(updatedCount: Int = 0, unreadCount: Int = 0) {
|
||||||
|
self.updatedCount = max(updatedCount, 0)
|
||||||
|
self.unreadCount = max(unreadCount, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
updatedCount = max(try container.decodeLossyInt(forKey: .updatedCount) ?? 0, 0)
|
||||||
|
unreadCount = max(try container.decodeLossyInt(forKey: .unreadCount) ?? 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 消息中心列表响应,对齐 Android `MsgResponse`。
|
/// 消息中心列表响应,对齐 Android `MsgResponse`。
|
||||||
struct MessageListResponse: Decodable, Equatable, Sendable {
|
struct MessageListResponse: Decodable, Equatable, Sendable {
|
||||||
let hasMore: Bool
|
let hasMore: Bool
|
||||||
|
|||||||
@ -11,11 +11,13 @@ final class MessageCenterViewModel {
|
|||||||
private(set) var isLoading = false
|
private(set) var isLoading = false
|
||||||
private(set) var isRefreshing = false
|
private(set) var isRefreshing = false
|
||||||
private(set) var isLoadingMore = false
|
private(set) var isLoadingMore = false
|
||||||
|
private(set) var isMarkingAllAsRead = false
|
||||||
private(set) var canLoadMore = false
|
private(set) var canLoadMore = false
|
||||||
|
|
||||||
var onStateChange: (() -> Void)?
|
var onStateChange: (() -> Void)?
|
||||||
var onShowMessage: ((String) -> Void)?
|
var onShowMessage: ((String) -> Void)?
|
||||||
var onOpenDetail: ((MessageItem) -> Void)?
|
var onOpenDetail: ((MessageItem) -> Void)?
|
||||||
|
var onUnreadMessageCountChange: ((Int) -> Void)?
|
||||||
|
|
||||||
private let pageSize = 20
|
private let pageSize = 20
|
||||||
private var lastId = 0
|
private var lastId = 0
|
||||||
@ -55,9 +57,10 @@ final class MessageCenterViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try await api.markAsRead(messageId: id)
|
let response = try await api.markAsRead(messageId: id)
|
||||||
let readItem = item.markedRead()
|
let readItem = item.markedRead()
|
||||||
items = items.map { $0.id == id ? readItem : $0 }
|
items = items.map { $0.id == id ? readItem : $0 }
|
||||||
|
notifyUnreadMessageCountChanged(response.unreadCount)
|
||||||
onOpenDetail?(readItem)
|
onOpenDetail?(readItem)
|
||||||
} catch is CancellationError {
|
} catch is CancellationError {
|
||||||
return
|
return
|
||||||
@ -66,6 +69,30 @@ final class MessageCenterViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 将当前账号的全部消息标记已读,成功时返回服务端最新统计。
|
||||||
|
func markAllAsRead(api: any MessageCenterServing) async -> MessageReadAllResponse? {
|
||||||
|
guard !isLoading, !isMarkingAllAsRead else { return nil }
|
||||||
|
isMarkingAllAsRead = true
|
||||||
|
notifyStateChange()
|
||||||
|
defer {
|
||||||
|
isMarkingAllAsRead = false
|
||||||
|
notifyStateChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let response = try await api.markAllAsRead()
|
||||||
|
items = items.map { $0.isRead ? $0 : $0.markedRead() }
|
||||||
|
notifyUnreadMessageCountChanged(response.unreadCount)
|
||||||
|
onShowMessage?(response.updatedCount > 0 ? "已全部标记为已读" : "暂无未读消息")
|
||||||
|
return response
|
||||||
|
} catch is CancellationError {
|
||||||
|
return nil
|
||||||
|
} catch {
|
||||||
|
onShowMessage?("全部标记已读失败,请稍后重试")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 删除后从本地列表移除消息。
|
/// 删除后从本地列表移除消息。
|
||||||
func removeMessage(id: Int) {
|
func removeMessage(id: Int) {
|
||||||
items.removeAll { $0.id == id }
|
items.removeAll { $0.id == id }
|
||||||
@ -110,6 +137,16 @@ final class MessageCenterViewModel {
|
|||||||
private func notifyStateChange() {
|
private func notifyStateChange() {
|
||||||
onStateChange?()
|
onStateChange?()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func notifyUnreadMessageCountChanged(_ count: Int) {
|
||||||
|
let normalizedCount = max(count, 0)
|
||||||
|
onUnreadMessageCountChange?(normalizedCount)
|
||||||
|
NotificationCenter.default.post(
|
||||||
|
name: NotificationName.unreadMessageCountDidChange,
|
||||||
|
object: nil,
|
||||||
|
userInfo: [NotificationUserInfoKey.unreadCount: normalizedCount]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 消息详情页 ViewModel,负责删除当前消息。
|
/// 消息详情页 ViewModel,负责删除当前消息。
|
||||||
|
|||||||
@ -25,6 +25,17 @@ final class PaymentAPI {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 拉取当前景区收款页的背景、Logo 与标题配置。
|
||||||
|
func payPageConfig(scenicId: Int) async throws -> PayPageConfigResponse {
|
||||||
|
try await client.send(
|
||||||
|
APIRequest(
|
||||||
|
method: .get,
|
||||||
|
path: "/api/yf-handset-app/photog/pay-page-config",
|
||||||
|
queryItems: [URLQueryItem(name: "scenic_id", value: String(scenicId))]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// 拉取近 7 天收款记录。
|
/// 拉取近 7 天收款记录。
|
||||||
func collectionRecord(scenicId: Int) async throws -> RepaymentCollectionRecordResponse {
|
func collectionRecord(scenicId: Int) async throws -> RepaymentCollectionRecordResponse {
|
||||||
try await client.send(
|
try await client.send(
|
||||||
|
|||||||
@ -5,6 +5,83 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
/// 收款页展示配置接口响应。
|
||||||
|
struct PayPageConfigResponse: Decodable, Sendable, Equatable {
|
||||||
|
let background: String
|
||||||
|
let logo: String
|
||||||
|
let title: String
|
||||||
|
let subtitle: String
|
||||||
|
|
||||||
|
init(background: String, logo: String, title: String, subtitle: String) {
|
||||||
|
self.background = background
|
||||||
|
self.logo = logo
|
||||||
|
self.title = title
|
||||||
|
self.subtitle = subtitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 收款页最终可展示并可持久化的品牌配置。
|
||||||
|
struct PayPageConfig: Codable, Sendable, Equatable {
|
||||||
|
let background: String
|
||||||
|
let logo: String
|
||||||
|
let title: String
|
||||||
|
let subtitle: String
|
||||||
|
|
||||||
|
/// 那拉提景区随包默认配置。
|
||||||
|
static let nalatiDefault = PayPageConfig(
|
||||||
|
background: "",
|
||||||
|
logo: "",
|
||||||
|
title: "那拉提旅拍管理收费平台",
|
||||||
|
subtitle: "那拉提景区"
|
||||||
|
)
|
||||||
|
|
||||||
|
/// 将服务端非空有效字段合并到当前可用配置。
|
||||||
|
func merging(_ response: PayPageConfigResponse) -> PayPageConfig {
|
||||||
|
PayPageConfig(
|
||||||
|
background: Self.validRemoteImageURL(response.background) ?? background,
|
||||||
|
logo: Self.validRemoteImageURL(response.logo) ?? logo,
|
||||||
|
title: response.title.trimmedNonEmpty ?? title,
|
||||||
|
subtitle: response.subtitle.trimmedNonEmpty ?? subtitle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func validRemoteImageURL(_ value: String) -> String? {
|
||||||
|
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard let url = URL(string: trimmed),
|
||||||
|
url.scheme?.lowercased() == "https",
|
||||||
|
url.host != nil else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 收款页品牌化景区判定策略。
|
||||||
|
enum PayPageBrandingPolicy {
|
||||||
|
/// 那拉提景区正式 ID。
|
||||||
|
static let nalatiScenicId = 128
|
||||||
|
|
||||||
|
/// 判断当前景区是否启用品牌收款页。
|
||||||
|
static func isNalati(scenicId: Int) -> Bool {
|
||||||
|
scenicId == nalatiScenicId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 收款页当前账号信息的展示文本格式化规则。
|
||||||
|
enum PaymentAccountDisplayFormatter {
|
||||||
|
/// 解析当前登录摄影师名称,昵称为空时使用实名,仍为空时显示占位符。
|
||||||
|
static func photographerName(userName: String, realName: String) -> String {
|
||||||
|
userName.trimmedNonEmpty ?? realName.trimmedNonEmpty ?? "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 去除空值与重复项后,合并当前账号的全部店铺名称。
|
||||||
|
static func storeNames(_ names: [String]) -> String {
|
||||||
|
var seen = Set<String>()
|
||||||
|
let resolved = names.compactMap(\.trimmedNonEmpty).filter { seen.insert($0).inserted }
|
||||||
|
return resolved.isEmpty ? "-" : resolved.joined(separator: "、")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 摄影师收款码响应,包含静态与动态支付 URL。
|
/// 摄影师收款码响应,包含静态与动态支付 URL。
|
||||||
struct PayCodeResponse: Decodable, Sendable, Equatable {
|
struct PayCodeResponse: Decodable, Sendable, Equatable {
|
||||||
let staticPayURL: String
|
let staticPayURL: String
|
||||||
@ -101,3 +178,10 @@ struct PaymentMerchantInfo: Sendable, Equatable {
|
|||||||
company: "扬州元智享网络科技有限公司"
|
company: "扬州元智享网络科技有限公司"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
var trimmedNonEmpty: String? {
|
||||||
|
let value = trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return value.isEmpty ? nil : value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
51
suixinkan/Features/Payment/Services/PayPageConfigCache.swift
Normal file
51
suixinkan/Features/Payment/Services/PayPageConfigCache.swift
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
//
|
||||||
|
// PayPageConfigCache.swift
|
||||||
|
// suixinkan
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// 收款页品牌配置缓存协议,供 ViewModel 注入持久化实现或测试替身。
|
||||||
|
protocol PayPageConfigCaching {
|
||||||
|
/// 读取指定景区的品牌配置。
|
||||||
|
func config(scenicId: Int) -> PayPageConfig?
|
||||||
|
|
||||||
|
/// 保存指定景区的完整品牌配置。
|
||||||
|
func save(_ config: PayPageConfig, scenicId: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用 UserDefaults 按接口环境与景区隔离的收款页品牌配置缓存。
|
||||||
|
final class PayPageConfigCache: PayPageConfigCaching {
|
||||||
|
private static let version = "v1"
|
||||||
|
|
||||||
|
private let defaults: UserDefaults
|
||||||
|
private let environmentNamespace: String
|
||||||
|
private let encoder = JSONEncoder()
|
||||||
|
private let decoder = JSONDecoder()
|
||||||
|
|
||||||
|
/// 使用指定持久化容器和 API 环境创建缓存。
|
||||||
|
init(
|
||||||
|
defaults: UserDefaults = .standard,
|
||||||
|
environment: APIEnvironment = .current
|
||||||
|
) {
|
||||||
|
self.defaults = defaults
|
||||||
|
environmentNamespace = environment.baseURL.host ?? environment.baseURL.absoluteString
|
||||||
|
}
|
||||||
|
|
||||||
|
func config(scenicId: Int) -> PayPageConfig? {
|
||||||
|
guard scenicId > 0,
|
||||||
|
let data = defaults.data(forKey: key(scenicId: scenicId)) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return try? decoder.decode(PayPageConfig.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func save(_ config: PayPageConfig, scenicId: Int) {
|
||||||
|
guard scenicId > 0, let data = try? encoder.encode(config) else { return }
|
||||||
|
defaults.set(data, forKey: key(scenicId: scenicId))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func key(scenicId: Int) -> String {
|
||||||
|
"payment.pay_page_config.\(Self.version).\(environmentNamespace).\(scenicId)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,6 +17,9 @@ final class PaymentCollectionDetailsViewModel {
|
|||||||
private(set) var showAmountDialog = false
|
private(set) var showAmountDialog = false
|
||||||
private(set) var amount = ""
|
private(set) var amount = ""
|
||||||
private(set) var remark = ""
|
private(set) var remark = ""
|
||||||
|
private(set) var scenicId = 0
|
||||||
|
private(set) var payPageConfig = PayPageConfig.nalatiDefault
|
||||||
|
private(set) var brandingRefreshVersion = 0
|
||||||
|
|
||||||
let merchantInfo = PaymentMerchantInfo.defaultInfo
|
let merchantInfo = PaymentMerchantInfo.defaultInfo
|
||||||
|
|
||||||
@ -26,9 +29,21 @@ final class PaymentCollectionDetailsViewModel {
|
|||||||
private var staticPayURL = ""
|
private var staticPayURL = ""
|
||||||
private var dynamicPayURL = ""
|
private var dynamicPayURL = ""
|
||||||
private let appStore: AppStore
|
private let appStore: AppStore
|
||||||
|
private let payPageConfigCache: PayPageConfigCaching
|
||||||
|
|
||||||
init(appStore: AppStore = .shared) {
|
init(
|
||||||
|
appStore: AppStore = .shared,
|
||||||
|
payPageConfigCache: PayPageConfigCaching = PayPageConfigCache()
|
||||||
|
) {
|
||||||
self.appStore = appStore
|
self.appStore = appStore
|
||||||
|
self.payPageConfigCache = payPageConfigCache
|
||||||
|
scenicId = appStore.session.currentScenicId
|
||||||
|
scenicName = appStore.session.currentScenicName
|
||||||
|
staffId = appStore.session.userId
|
||||||
|
isVoiceBroadcastOpen = appStore.payment.isOpenReceiveVoice
|
||||||
|
if PayPageBrandingPolicy.isNalati(scenicId: scenicId) {
|
||||||
|
payPageConfig = payPageConfigCache.config(scenicId: scenicId) ?? .nalatiDefault
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasPayCode: Bool {
|
var hasPayCode: Bool {
|
||||||
@ -39,10 +54,32 @@ final class PaymentCollectionDetailsViewModel {
|
|||||||
scenicName.isEmpty ? "暂无景区" : scenicName
|
scenicName.isEmpty ? "暂无景区" : scenicName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var usesNalatiBranding: Bool {
|
||||||
|
PayPageBrandingPolicy.isNalati(scenicId: scenicId)
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayPhotographerName: String {
|
||||||
|
PaymentAccountDisplayFormatter.photographerName(
|
||||||
|
userName: appStore.session.userName,
|
||||||
|
realName: appStore.session.realName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayStoreNames: String {
|
||||||
|
let names = appStore.permissions.rolePermissionList().flatMap(\.store).map(\.name)
|
||||||
|
return PaymentAccountDisplayFormatter.storeNames(names)
|
||||||
|
}
|
||||||
|
|
||||||
func refreshLocalState() {
|
func refreshLocalState() {
|
||||||
|
scenicId = appStore.session.currentScenicId
|
||||||
scenicName = appStore.session.currentScenicName
|
scenicName = appStore.session.currentScenicName
|
||||||
staffId = appStore.session.userId
|
staffId = appStore.session.userId
|
||||||
isVoiceBroadcastOpen = appStore.payment.isOpenReceiveVoice
|
isVoiceBroadcastOpen = appStore.payment.isOpenReceiveVoice
|
||||||
|
if usesNalatiBranding {
|
||||||
|
payPageConfig = payPageConfigCache.config(scenicId: scenicId) ?? .nalatiDefault
|
||||||
|
} else {
|
||||||
|
payPageConfig = .nalatiDefault
|
||||||
|
}
|
||||||
notifyStateChange()
|
notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +93,7 @@ final class PaymentCollectionDetailsViewModel {
|
|||||||
notifyStateChange()
|
notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
let scenicId = appStore.session.currentScenicId
|
let scenicId = self.scenicId
|
||||||
guard scenicId > 0 else {
|
guard scenicId > 0 else {
|
||||||
clearPayCode()
|
clearPayCode()
|
||||||
return
|
return
|
||||||
@ -72,8 +109,27 @@ final class PaymentCollectionDetailsViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadPayPageConfig(api: PaymentAPI) async {
|
||||||
|
let requestedScenicId = appStore.session.currentScenicId
|
||||||
|
guard PayPageBrandingPolicy.isNalati(scenicId: requestedScenicId) else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let response = try await api.payPageConfig(scenicId: requestedScenicId)
|
||||||
|
guard appStore.session.currentScenicId == requestedScenicId else { return }
|
||||||
|
let resolved = payPageConfig.merging(response)
|
||||||
|
payPageConfigCache.save(resolved, scenicId: requestedScenicId)
|
||||||
|
payPageConfig = resolved
|
||||||
|
brandingRefreshVersion += 1
|
||||||
|
notifyStateChange()
|
||||||
|
} catch {
|
||||||
|
// 品牌配置失败时保留缓存或随包默认值,不影响收款码与页面交互。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func refresh(api: PaymentAPI) async {
|
func refresh(api: PaymentAPI) async {
|
||||||
await loadPayCode(api: api)
|
async let payCode: Void = loadPayCode(api: api)
|
||||||
|
async let pageConfig: Void = loadPayPageConfig(api: api)
|
||||||
|
_ = await (payCode, pageConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showSetAmountDialog() {
|
func showSetAmountDialog() {
|
||||||
|
|||||||
@ -42,5 +42,9 @@
|
|||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>remote-notification</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -24,7 +24,18 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
self.window = window
|
self.window = window
|
||||||
(UIApplication.shared.delegate as? AppDelegate)?.window = window
|
(UIApplication.shared.delegate as? AppDelegate)?.window = window
|
||||||
|
|
||||||
|
PushNotificationManager.shared.attach(window: window)
|
||||||
|
|
||||||
registerNotifications()
|
registerNotifications()
|
||||||
|
|
||||||
|
if AppStore.shared.session.isLoggedIn {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
PushNotificationManager.shared.handleLoginCompleted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let response = connectionOptions.notificationResponse {
|
||||||
|
PushNotificationManager.shared.handleNotificationResponse(response)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sceneDidDisconnect(_ scene: UIScene) {
|
func sceneDidDisconnect(_ scene: UIScene) {
|
||||||
@ -43,6 +54,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
_ = WeChatManager.shared.handleContinueUserActivity(userActivity)
|
_ = WeChatManager.shared.handleContinueUserActivity(userActivity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||||
|
PushNotificationManager.shared.retryPendingRegistrationUpload()
|
||||||
|
}
|
||||||
|
|
||||||
private func registerNotifications() {
|
private func registerNotifications() {
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
self,
|
self,
|
||||||
@ -62,19 +77,35 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
name: NotificationName.userDidLogin,
|
name: NotificationName.userDidLogin,
|
||||||
object: nil
|
object: nil
|
||||||
)
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(handleAccountDidSwitch),
|
||||||
|
name: NotificationName.accountDidSwitch,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handleSessionDidExpire() {
|
@objc private func handleSessionDidExpire() {
|
||||||
|
PushNotificationManager.shared.handleLogout()
|
||||||
AppStore.shared.logout()
|
AppStore.shared.logout()
|
||||||
AppRouter.setRoot(.login, on: window)
|
AppRouter.setRoot(.login, on: window)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handleUserDidLogout() {
|
@objc private func handleUserDidLogout() {
|
||||||
|
PushNotificationManager.shared.handleLogout()
|
||||||
AppStore.shared.logout()
|
AppStore.shared.logout()
|
||||||
AppRouter.setRoot(.login, on: window)
|
AppRouter.setRoot(.login, on: window)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handleUserDidLogin() {
|
@objc private func handleUserDidLogin() {
|
||||||
AppRouter.setRoot(.mainTab, on: window)
|
AppRouter.setRoot(.mainTab, on: window)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
PushNotificationManager.shared.handleLoginCompleted()
|
||||||
|
PushNotificationManager.shared.routePendingNotificationIfPossible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handleAccountDidSwitch() {
|
||||||
|
PushNotificationManager.shared.handleAccountSwitched()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ final class HomeViewController: BaseViewController {
|
|||||||
|
|
||||||
private let viewModel = HomeViewModel()
|
private let viewModel = HomeViewModel()
|
||||||
private let homeAPI = NetworkServices.shared.homeAPI
|
private let homeAPI = NetworkServices.shared.homeAPI
|
||||||
|
private let messageCenterAPI = NetworkServices.shared.messageCenterAPI
|
||||||
|
|
||||||
private let scenicHeaderView = HomeScenicHeaderView()
|
private let scenicHeaderView = HomeScenicHeaderView()
|
||||||
private var collectionView: UICollectionView!
|
private var collectionView: UICollectionView!
|
||||||
@ -74,6 +75,7 @@ final class HomeViewController: BaseViewController {
|
|||||||
Task { @MainActor in self?.showToast(message) }
|
Task { @MainActor in self?.showToast(message) }
|
||||||
}
|
}
|
||||||
scenicHeaderView.onTap = { [weak self] in self?.presentScenicSelection() }
|
scenicHeaderView.onTap = { [weak self] in self?.presentScenicSelection() }
|
||||||
|
scenicHeaderView.onMessageTap = { [weak self] in self?.openMessageCenter() }
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
@ -88,11 +90,26 @@ final class HomeViewController: BaseViewController {
|
|||||||
name: NotificationName.scenicDidChange,
|
name: NotificationName.scenicDidChange,
|
||||||
object: nil
|
object: nil
|
||||||
)
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(handleUnreadMessageCountDidChange(_:)),
|
||||||
|
name: NotificationName.unreadMessageCountDidChange,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(handleUnreadMessageCountDidChange(_:)),
|
||||||
|
name: UIApplication.didBecomeActiveNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
navigationController?.setNavigationBarHidden(true, animated: animated)
|
navigationController?.setNavigationBarHidden(true, animated: animated)
|
||||||
|
if hasInitialized {
|
||||||
|
Task { await refreshUnreadMessageStatus() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
@ -272,6 +289,7 @@ final class HomeViewController: BaseViewController {
|
|||||||
private func initializeHome() async {
|
private func initializeHome() async {
|
||||||
showLoading()
|
showLoading()
|
||||||
await viewModel.initialize(api: homeAPI)
|
await viewModel.initialize(api: homeAPI)
|
||||||
|
await refreshUnreadMessageStatus()
|
||||||
hideLoading()
|
hideLoading()
|
||||||
|
|
||||||
applyViewModel()
|
applyViewModel()
|
||||||
@ -287,7 +305,10 @@ final class HomeViewController: BaseViewController {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func applyViewModel() {
|
private func applyViewModel() {
|
||||||
scenicHeaderView.apply(scenicName: viewModel.currentScenicName)
|
scenicHeaderView.apply(
|
||||||
|
scenicName: viewModel.currentScenicName,
|
||||||
|
hasUnreadMessages: viewModel.hasUnreadMessages
|
||||||
|
)
|
||||||
|
|
||||||
let showWorkBlock = !viewModel.isMinimalTopRole
|
let showWorkBlock = !viewModel.isMinimalTopRole
|
||||||
let showStore = viewModel.currentAppRole == .storeAdmin && viewModel.storeItem != nil
|
let showStore = viewModel.currentAppRole == .storeAdmin && viewModel.storeItem != nil
|
||||||
@ -499,6 +520,10 @@ final class HomeViewController: BaseViewController {
|
|||||||
pushScenicSelection()
|
pushScenicSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func openMessageCenter() {
|
||||||
|
navigationController?.pushViewController(MessageCenterViewController(), animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
private func pushScenicSelection() {
|
private func pushScenicSelection() {
|
||||||
navigationController?.setNavigationBarHidden(false, animated: true)
|
navigationController?.setNavigationBarHidden(false, animated: true)
|
||||||
let controller = ScenicSelectionViewController()
|
let controller = ScenicSelectionViewController()
|
||||||
@ -529,6 +554,7 @@ final class HomeViewController: BaseViewController {
|
|||||||
viewModel.markNeedsPermissionReload()
|
viewModel.markNeedsPermissionReload()
|
||||||
Task {
|
Task {
|
||||||
await viewModel.reloadIfNeeded(api: homeAPI)
|
await viewModel.reloadIfNeeded(api: homeAPI)
|
||||||
|
await refreshUnreadMessageStatus()
|
||||||
applyViewModel()
|
applyViewModel()
|
||||||
await evaluateDialogsWithDelay()
|
await evaluateDialogsWithDelay()
|
||||||
}
|
}
|
||||||
@ -541,6 +567,21 @@ final class HomeViewController: BaseViewController {
|
|||||||
applyViewModel()
|
applyViewModel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func handleUnreadMessageCountDidChange(_ notification: Notification) {
|
||||||
|
guard hasInitialized else { return }
|
||||||
|
if let count = notification.userInfo?[NotificationUserInfoKey.unreadCount] as? Int {
|
||||||
|
viewModel.updateUnreadMessageCount(count)
|
||||||
|
Task { await PushNotificationManager.shared.updateApplicationIconBadgeCount(count) }
|
||||||
|
} else {
|
||||||
|
Task { await refreshUnreadMessageStatus() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshUnreadMessageStatus() async {
|
||||||
|
guard await viewModel.refreshUnreadMessageStatus(api: messageCenterAPI) else { return }
|
||||||
|
await PushNotificationManager.shared.updateApplicationIconBadgeCount(viewModel.unreadMessageCount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension HomeViewController: UICollectionViewDelegate {
|
extension HomeViewController: UICollectionViewDelegate {
|
||||||
|
|||||||
@ -9,10 +9,16 @@ import UIKit
|
|||||||
/// 首页顶部景区选择条。
|
/// 首页顶部景区选择条。
|
||||||
final class HomeScenicHeaderView: UIView {
|
final class HomeScenicHeaderView: UIView {
|
||||||
|
|
||||||
|
/// 点击景区名称时触发。
|
||||||
var onTap: (() -> Void)?
|
var onTap: (() -> Void)?
|
||||||
|
|
||||||
|
/// 点击消息入口时触发。
|
||||||
|
var onMessageTap: (() -> Void)?
|
||||||
|
|
||||||
private let titleLabel = UILabel()
|
private let titleLabel = UILabel()
|
||||||
private let arrowView = UIImageView(image: UIImage(systemName: "chevron.down"))
|
private let arrowView = UIImageView(image: UIImage(systemName: "chevron.down"))
|
||||||
|
private let messageButton = UIButton(type: .system)
|
||||||
|
private let unreadDotView = UIView()
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
@ -25,8 +31,24 @@ final class HomeScenicHeaderView: UIView {
|
|||||||
arrowView.contentMode = .scaleAspectFit
|
arrowView.contentMode = .scaleAspectFit
|
||||||
arrowView.setContentCompressionResistancePriority(.required, for: .horizontal)
|
arrowView.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
|
||||||
|
var messageConfiguration = UIButton.Configuration.plain()
|
||||||
|
messageConfiguration.image = UIImage(systemName: "bell")
|
||||||
|
messageConfiguration.baseForegroundColor = AppColor.textPrimary
|
||||||
|
messageConfiguration.contentInsets = .zero
|
||||||
|
messageButton.configuration = messageConfiguration
|
||||||
|
messageButton.accessibilityIdentifier = "home_message_button"
|
||||||
|
messageButton.addTarget(self, action: #selector(handleMessageTap), for: .touchUpInside)
|
||||||
|
|
||||||
|
unreadDotView.backgroundColor = AppColor.danger
|
||||||
|
unreadDotView.layer.cornerRadius = 4
|
||||||
|
unreadDotView.isUserInteractionEnabled = false
|
||||||
|
unreadDotView.isHidden = true
|
||||||
|
unreadDotView.accessibilityIdentifier = "home_message_unread_dot"
|
||||||
|
|
||||||
addSubview(titleLabel)
|
addSubview(titleLabel)
|
||||||
addSubview(arrowView)
|
addSubview(arrowView)
|
||||||
|
addSubview(messageButton)
|
||||||
|
messageButton.addSubview(unreadDotView)
|
||||||
|
|
||||||
titleLabel.snp.makeConstraints { make in
|
titleLabel.snp.makeConstraints { make in
|
||||||
make.leading.equalTo(safeAreaLayoutGuide).offset(AppSpacing.screenHorizontalInset)
|
make.leading.equalTo(safeAreaLayoutGuide).offset(AppSpacing.screenHorizontalInset)
|
||||||
@ -34,12 +56,23 @@ final class HomeScenicHeaderView: UIView {
|
|||||||
}
|
}
|
||||||
arrowView.snp.makeConstraints { make in
|
arrowView.snp.makeConstraints { make in
|
||||||
make.leading.equalTo(titleLabel.snp.trailing).offset(AppSpacing.xs)
|
make.leading.equalTo(titleLabel.snp.trailing).offset(AppSpacing.xs)
|
||||||
make.trailing.lessThanOrEqualTo(safeAreaLayoutGuide).offset(-AppSpacing.screenHorizontalInset)
|
make.trailing.lessThanOrEqualTo(messageButton.snp.leading).offset(-AppSpacing.xs)
|
||||||
make.centerY.equalTo(safeAreaLayoutGuide)
|
make.centerY.equalTo(safeAreaLayoutGuide)
|
||||||
make.width.height.equalTo(16)
|
make.width.height.equalTo(16)
|
||||||
}
|
}
|
||||||
|
messageButton.snp.makeConstraints { make in
|
||||||
|
make.trailing.equalTo(safeAreaLayoutGuide).offset(-AppSpacing.screenHorizontalInset)
|
||||||
|
make.centerY.equalTo(safeAreaLayoutGuide)
|
||||||
|
make.width.height.equalTo(44)
|
||||||
|
}
|
||||||
|
unreadDotView.snp.makeConstraints { make in
|
||||||
|
make.centerX.equalTo(messageButton.snp.centerX).offset(9)
|
||||||
|
make.centerY.equalTo(messageButton.snp.centerY).offset(-9)
|
||||||
|
make.width.height.equalTo(8)
|
||||||
|
}
|
||||||
|
|
||||||
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
||||||
|
tap.delegate = self
|
||||||
addGestureRecognizer(tap)
|
addGestureRecognizer(tap)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,12 +81,26 @@ final class HomeScenicHeaderView: UIView {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func apply(scenicName: String) {
|
/// 更新景区名称与未读消息状态。
|
||||||
|
func apply(scenicName: String, hasUnreadMessages: Bool) {
|
||||||
let trimmed = scenicName.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = scenicName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
titleLabel.text = trimmed.isEmpty ? "请选择景区" : trimmed
|
titleLabel.text = trimmed.isEmpty ? "请选择景区" : trimmed
|
||||||
|
unreadDotView.isHidden = !hasUnreadMessages
|
||||||
|
messageButton.accessibilityLabel = hasUnreadMessages ? "消息中心,有未读消息" : "消息中心"
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handleTap() {
|
@objc private func handleTap() {
|
||||||
onTap?()
|
onTap?()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func handleMessageTap() {
|
||||||
|
onMessageTap?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HomeScenicHeaderView: UIGestureRecognizerDelegate {
|
||||||
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||||
|
guard let touchedView = touch.view else { return true }
|
||||||
|
return touchedView !== messageButton && !touchedView.isDescendant(of: messageButton)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,6 +65,29 @@ final class MainTabBarController: UITabBarController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 响应推送点击,切换到目标 Tab 并打开对应业务页面。
|
||||||
|
func openPushDestination(_ destination: PushDestination) {
|
||||||
|
let targetTab = AppTab.home
|
||||||
|
|
||||||
|
selectTab(targetTab)
|
||||||
|
guard let navigationController = tabNavigationControllers[targetTab] else { return }
|
||||||
|
_ = navigationController.popToRootViewController(animated: false)
|
||||||
|
|
||||||
|
let controller: UIViewController?
|
||||||
|
switch destination {
|
||||||
|
case .paymentRecord:
|
||||||
|
controller = PaymentCollectionRecordViewController()
|
||||||
|
case .paymentDetails:
|
||||||
|
controller = PaymentCollectionDetailsViewController()
|
||||||
|
case .messageCenter:
|
||||||
|
controller = MessageCenterViewController()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let controller {
|
||||||
|
navigationController.pushViewController(controller, animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func configureTabBarAppearance() {
|
private func configureTabBarAppearance() {
|
||||||
tabBar.tintColor = AppColor.primary
|
tabBar.tintColor = AppColor.primary
|
||||||
tabBar.unselectedItemTintColor = AppColor.textTabInactive
|
tabBar.unselectedItemTintColor = AppColor.textTabInactive
|
||||||
|
|||||||
@ -19,6 +19,12 @@ final class MessageCenterViewController: BaseViewController, UITableViewDelegate
|
|||||||
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
|
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
|
||||||
private let footerSpinner = UIActivityIndicatorView(style: .medium)
|
private let footerSpinner = UIActivityIndicatorView(style: .medium)
|
||||||
private let emptyLabel = UILabel()
|
private let emptyLabel = UILabel()
|
||||||
|
private lazy var markAllReadButton = UIBarButtonItem(
|
||||||
|
title: "全部已读",
|
||||||
|
style: .plain,
|
||||||
|
target: self,
|
||||||
|
action: #selector(markAllReadTapped)
|
||||||
|
)
|
||||||
|
|
||||||
private var dataSource: UITableViewDiffableDataSource<Section, MessageItem>!
|
private var dataSource: UITableViewDiffableDataSource<Section, MessageItem>!
|
||||||
|
|
||||||
@ -46,6 +52,7 @@ final class MessageCenterViewController: BaseViewController, UITableViewDelegate
|
|||||||
|
|
||||||
override func setupNavigationBar() {
|
override func setupNavigationBar() {
|
||||||
title = "消息中心"
|
title = "消息中心"
|
||||||
|
navigationItem.rightBarButtonItem = markAllReadButton
|
||||||
}
|
}
|
||||||
|
|
||||||
override func setupUI() {
|
override func setupUI() {
|
||||||
@ -79,7 +86,8 @@ final class MessageCenterViewController: BaseViewController, UITableViewDelegate
|
|||||||
|
|
||||||
override func setupConstraints() {
|
override func setupConstraints() {
|
||||||
tableView.snp.makeConstraints { make in
|
tableView.snp.makeConstraints { make in
|
||||||
make.edges.equalTo(view.safeAreaLayoutGuide)
|
make.top.equalTo(view.safeAreaLayoutGuide)
|
||||||
|
make.leading.trailing.bottom.equalToSuperview()
|
||||||
}
|
}
|
||||||
loadingIndicator.snp.makeConstraints { make in
|
loadingIndicator.snp.makeConstraints { make in
|
||||||
make.center.equalToSuperview()
|
make.center.equalToSuperview()
|
||||||
@ -103,6 +111,11 @@ final class MessageCenterViewController: BaseViewController, UITableViewDelegate
|
|||||||
self?.openDetail(message)
|
self?.openDetail(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
viewModel.onUnreadMessageCountChange = { count in
|
||||||
|
Task { @MainActor in
|
||||||
|
await PushNotificationManager.shared.updateApplicationIconBadgeCount(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
@ -125,6 +138,12 @@ final class MessageCenterViewController: BaseViewController, UITableViewDelegate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func markAllReadTapped() {
|
||||||
|
Task { @MainActor in
|
||||||
|
await viewModel.markAllAsRead(api: api)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func configureDataSource() {
|
private func configureDataSource() {
|
||||||
dataSource = UITableViewDiffableDataSource<Section, MessageItem>(tableView: tableView) { tableView, indexPath, item in
|
dataSource = UITableViewDiffableDataSource<Section, MessageItem>(tableView: tableView) { tableView, indexPath, item in
|
||||||
let cell = tableView.dequeueReusableCell(
|
let cell = tableView.dequeueReusableCell(
|
||||||
@ -154,6 +173,7 @@ final class MessageCenterViewController: BaseViewController, UITableViewDelegate
|
|||||||
}
|
}
|
||||||
|
|
||||||
tableView.tableFooterView = footerView()
|
tableView.tableFooterView = footerView()
|
||||||
|
markAllReadButton.isEnabled = !viewModel.isLoading && !viewModel.isMarkingAllAsRead
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applySnapshot(animated: Bool) {
|
private func applySnapshot(animated: Bool) {
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
// suixinkan
|
// suixinkan
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Kingfisher
|
||||||
import SnapKit
|
import SnapKit
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@ -13,10 +14,18 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
private let paymentAPI = NetworkServices.shared.paymentAPI
|
private let paymentAPI = NetworkServices.shared.paymentAPI
|
||||||
|
|
||||||
private let scrollView = UIScrollView()
|
private let scrollView = UIScrollView()
|
||||||
|
private let contentContainerView = UIView()
|
||||||
private let contentStack = UIStackView()
|
private let contentStack = UIStackView()
|
||||||
|
|
||||||
|
private let backgroundImageView = UIImageView()
|
||||||
|
private let brandHeaderStack = UIStackView()
|
||||||
|
private let brandLogoImageView = UIImageView()
|
||||||
|
private let brandTitleLabel = UILabel()
|
||||||
|
private let brandSubtitleLabel = UILabel()
|
||||||
|
|
||||||
private let qrCardView = UIView()
|
private let qrCardView = UIView()
|
||||||
private let scenicNameLabel = UILabel()
|
private let scenicNameLabel = UILabel()
|
||||||
|
private let qrContainerView = UIView()
|
||||||
private let qrImageView = UIImageView()
|
private let qrImageView = UIImageView()
|
||||||
private let qrPlaceholderView = UIView()
|
private let qrPlaceholderView = UIView()
|
||||||
private let qrErrorLabel = UILabel()
|
private let qrErrorLabel = UILabel()
|
||||||
@ -29,26 +38,75 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
private let scenicRow = PaymentInfoRowView(title: "景区名称")
|
private let scenicRow = PaymentInfoRowView(title: "景区名称")
|
||||||
private let merchantRow = PaymentInfoRowView(title: "收款方")
|
private let merchantRow = PaymentInfoRowView(title: "收款方")
|
||||||
private let staffRow = PaymentInfoRowView(title: "员工 ID")
|
private let staffRow = PaymentInfoRowView(title: "员工 ID")
|
||||||
|
private let photographerRow = PaymentInfoRowView(title: "摄影师名称")
|
||||||
|
private let storeRow = PaymentInfoRowView(title: "店铺名称")
|
||||||
|
|
||||||
private let recordRow = PaymentNavigationRowView(title: "收款记录")
|
private let recordRow = PaymentNavigationRowView(title: "收款记录")
|
||||||
|
|
||||||
|
private let settingsCardView = UIView()
|
||||||
private let voiceCardView = UIView()
|
private let voiceCardView = UIView()
|
||||||
private let voiceTitleLabel = UILabel()
|
private let voiceTitleLabel = UILabel()
|
||||||
private let voiceSwitch = UISwitch()
|
private let voiceSwitch = UISwitch()
|
||||||
|
|
||||||
private var amountDialog: PaymentSetAmountDialogView?
|
private var amountDialog: PaymentSetAmountDialogView?
|
||||||
|
private var appliedBrandConfig: PayPageConfig?
|
||||||
|
private var appliedBrandingRefreshVersion = -1
|
||||||
|
|
||||||
|
private var previousStandardAppearance: UINavigationBarAppearance?
|
||||||
|
private var previousScrollEdgeAppearance: UINavigationBarAppearance?
|
||||||
|
private var previousCompactAppearance: UINavigationBarAppearance?
|
||||||
|
private var previousNavigationTintColor: UIColor?
|
||||||
|
private var previousNavigationTranslucency = false
|
||||||
|
|
||||||
|
private var brandScreenHeight: CGFloat {
|
||||||
|
UIScreen.main.bounds.height
|
||||||
|
}
|
||||||
|
|
||||||
|
private var brandLogoHeight: CGFloat {
|
||||||
|
if brandScreenHeight <= 700 { return 82 }
|
||||||
|
if brandScreenHeight <= 820 { return 96 }
|
||||||
|
return 112
|
||||||
|
}
|
||||||
|
|
||||||
|
private var brandQRContainerSize: CGFloat {
|
||||||
|
if brandScreenHeight <= 700 { return 170 }
|
||||||
|
if brandScreenHeight <= 820 { return 190 }
|
||||||
|
return 210
|
||||||
|
}
|
||||||
|
|
||||||
|
private var brandQRImageSize: CGFloat {
|
||||||
|
if brandScreenHeight <= 700 { return 146 }
|
||||||
|
if brandScreenHeight <= 820 { return 166 }
|
||||||
|
return 182
|
||||||
|
}
|
||||||
|
|
||||||
|
private var brandSectionSpacing: CGFloat {
|
||||||
|
if brandScreenHeight <= 700 { return 8 }
|
||||||
|
if brandScreenHeight <= 820 { return 10 }
|
||||||
|
return 12
|
||||||
|
}
|
||||||
|
|
||||||
|
private var brandHeaderToQRSpacing: CGFloat {
|
||||||
|
if brandScreenHeight <= 700 { return 16 }
|
||||||
|
if brandScreenHeight <= 820 { return 18 }
|
||||||
|
return 20
|
||||||
|
}
|
||||||
|
|
||||||
override func setupNavigationBar() {
|
override func setupNavigationBar() {
|
||||||
title = "收款详情"
|
title = viewModel.usesNalatiBranding ? nil : "收款详情"
|
||||||
}
|
}
|
||||||
|
|
||||||
override func setupUI() {
|
override func setupUI() {
|
||||||
view.backgroundColor = AppColor.pageBackground
|
let usesBranding = viewModel.usesNalatiBranding
|
||||||
|
view.backgroundColor = usesBranding ? .clear : AppColor.pageBackground
|
||||||
|
scrollView.showsVerticalScrollIndicator = !usesBranding
|
||||||
|
scrollView.isScrollEnabled = !usesBranding
|
||||||
|
scrollView.alwaysBounceVertical = false
|
||||||
|
|
||||||
contentStack.axis = .vertical
|
contentStack.axis = .vertical
|
||||||
contentStack.spacing = AppSpacing.md
|
contentStack.spacing = usesBranding ? brandSectionSpacing : AppSpacing.md
|
||||||
|
|
||||||
qrCardView.backgroundColor = .white
|
qrCardView.backgroundColor = usesBranding ? .clear : .white
|
||||||
qrCardView.layer.cornerRadius = AppRadius.lg
|
qrCardView.layer.cornerRadius = AppRadius.lg
|
||||||
|
|
||||||
scenicNameLabel.font = .systemFont(ofSize: 16, weight: .medium)
|
scenicNameLabel.font = .systemFont(ofSize: 16, weight: .medium)
|
||||||
@ -57,9 +115,16 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
|
|
||||||
qrImageView.contentMode = .scaleAspectFit
|
qrImageView.contentMode = .scaleAspectFit
|
||||||
qrImageView.isHidden = true
|
qrImageView.isHidden = true
|
||||||
|
if usesBranding {
|
||||||
|
qrContainerView.backgroundColor = .white
|
||||||
|
qrContainerView.layer.cornerRadius = 10
|
||||||
|
qrContainerView.clipsToBounds = true
|
||||||
|
qrContainerView.isHidden = true
|
||||||
|
qrContainerView.addSubview(qrImageView)
|
||||||
|
}
|
||||||
|
|
||||||
qrPlaceholderView.backgroundColor = AppColor.inputBackground
|
qrPlaceholderView.backgroundColor = AppColor.inputBackground
|
||||||
qrPlaceholderView.layer.cornerRadius = 24
|
qrPlaceholderView.layer.cornerRadius = usesBranding ? 10 : 24
|
||||||
qrPlaceholderView.isHidden = true
|
qrPlaceholderView.isHidden = true
|
||||||
|
|
||||||
qrErrorLabel.text = "未选景区或网络问题"
|
qrErrorLabel.text = "未选景区或网络问题"
|
||||||
@ -73,19 +138,29 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
|
|
||||||
actionRow.axis = .horizontal
|
actionRow.axis = .horizontal
|
||||||
actionRow.alignment = .center
|
actionRow.alignment = .center
|
||||||
actionRow.spacing = AppSpacing.xl
|
actionRow.spacing = usesBranding ? AppSpacing.lg : AppSpacing.xl
|
||||||
|
if usesBranding {
|
||||||
|
actionRow.backgroundColor = UIColor.white.withAlphaComponent(0.88)
|
||||||
|
actionRow.layer.cornerRadius = 24
|
||||||
|
actionRow.isLayoutMarginsRelativeArrangement = true
|
||||||
|
actionRow.directionalLayoutMargins = NSDirectionalEdgeInsets(
|
||||||
|
top: 10,
|
||||||
|
leading: 24,
|
||||||
|
bottom: 10,
|
||||||
|
trailing: 24
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
configureLinkButton(setAmountButton, title: "设置金额")
|
configureLinkButton(setAmountButton, title: "设置金额")
|
||||||
configureLinkButton(saveQRButton, title: "保存二维码")
|
configureLinkButton(saveQRButton, title: "保存二维码")
|
||||||
|
|
||||||
infoCardView.backgroundColor = .white
|
configureCard(infoCardView, branded: usesBranding)
|
||||||
infoCardView.layer.cornerRadius = AppRadius.lg
|
|
||||||
|
|
||||||
recordRow.backgroundColor = .white
|
recordRow.backgroundColor = usesBranding ? .clear : .white
|
||||||
recordRow.layer.cornerRadius = AppRadius.lg
|
recordRow.layer.cornerRadius = usesBranding ? 0 : AppRadius.lg
|
||||||
|
|
||||||
voiceCardView.backgroundColor = .white
|
voiceCardView.backgroundColor = usesBranding ? .clear : .white
|
||||||
voiceCardView.layer.cornerRadius = AppRadius.lg
|
voiceCardView.layer.cornerRadius = usesBranding ? 0 : AppRadius.lg
|
||||||
|
|
||||||
voiceTitleLabel.text = "收款到账语音提醒"
|
voiceTitleLabel.text = "收款到账语音提醒"
|
||||||
voiceTitleLabel.font = .systemFont(ofSize: 14)
|
voiceTitleLabel.font = .systemFont(ofSize: 14)
|
||||||
@ -93,49 +168,112 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
|
|
||||||
voiceSwitch.onTintColor = AppColor.primary
|
voiceSwitch.onTintColor = AppColor.primary
|
||||||
|
|
||||||
view.addSubview(scrollView)
|
if usesBranding {
|
||||||
scrollView.addSubview(contentStack)
|
configureBrandHeader()
|
||||||
|
backgroundImageView.contentMode = .scaleAspectFill
|
||||||
|
backgroundImageView.clipsToBounds = true
|
||||||
|
backgroundImageView.image = UIImage(named: "payment_nalati_background")
|
||||||
|
view.addSubview(backgroundImageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.addSubview(scrollView)
|
||||||
|
scrollView.addSubview(contentContainerView)
|
||||||
|
contentContainerView.addSubview(contentStack)
|
||||||
|
|
||||||
|
if usesBranding {
|
||||||
|
contentStack.addArrangedSubview(brandHeaderStack)
|
||||||
|
contentStack.setCustomSpacing(brandHeaderToQRSpacing, after: brandHeaderStack)
|
||||||
|
}
|
||||||
contentStack.addArrangedSubview(qrCardView)
|
contentStack.addArrangedSubview(qrCardView)
|
||||||
contentStack.addArrangedSubview(infoCardView)
|
contentStack.addArrangedSubview(infoCardView)
|
||||||
contentStack.addArrangedSubview(recordRow)
|
if usesBranding {
|
||||||
contentStack.addArrangedSubview(voiceCardView)
|
configureCard(settingsCardView, branded: true)
|
||||||
|
contentStack.addArrangedSubview(settingsCardView)
|
||||||
|
} else {
|
||||||
|
contentStack.addArrangedSubview(recordRow)
|
||||||
|
contentStack.addArrangedSubview(voiceCardView)
|
||||||
|
}
|
||||||
|
|
||||||
|
let qrDisplayView = usesBranding ? qrContainerView : qrImageView
|
||||||
let qrContentStack = UIStackView(arrangedSubviews: [
|
let qrContentStack = UIStackView(arrangedSubviews: [
|
||||||
scenicNameLabel,
|
scenicNameLabel,
|
||||||
qrImageView,
|
qrDisplayView,
|
||||||
qrPlaceholderView,
|
qrPlaceholderView,
|
||||||
actionRow,
|
actionRow,
|
||||||
])
|
])
|
||||||
qrContentStack.axis = .vertical
|
qrContentStack.axis = .vertical
|
||||||
qrContentStack.alignment = .center
|
qrContentStack.alignment = .center
|
||||||
qrContentStack.spacing = AppSpacing.lg
|
qrContentStack.spacing = usesBranding ? brandSectionSpacing : AppSpacing.lg
|
||||||
qrCardView.addSubview(qrContentStack)
|
qrCardView.addSubview(qrContentStack)
|
||||||
|
|
||||||
qrPlaceholderView.addSubview(qrErrorLabel)
|
qrPlaceholderView.addSubview(qrErrorLabel)
|
||||||
qrPlaceholderView.addSubview(refreshButton)
|
qrPlaceholderView.addSubview(refreshButton)
|
||||||
|
|
||||||
actionRow.addArrangedSubview(setAmountButton)
|
actionRow.addArrangedSubview(setAmountButton)
|
||||||
|
if usesBranding {
|
||||||
|
let divider = UIView()
|
||||||
|
divider.backgroundColor = UIColor(hex: 0xD6E1EE)
|
||||||
|
actionRow.addArrangedSubview(divider)
|
||||||
|
divider.snp.makeConstraints { make in
|
||||||
|
make.width.equalTo(1)
|
||||||
|
make.height.equalTo(24)
|
||||||
|
}
|
||||||
|
}
|
||||||
actionRow.addArrangedSubview(saveQRButton)
|
actionRow.addArrangedSubview(saveQRButton)
|
||||||
|
|
||||||
let infoStack = UIStackView(arrangedSubviews: [scenicRow, merchantRow, staffRow])
|
let infoRows = usesBranding
|
||||||
|
? [photographerRow, storeRow]
|
||||||
|
: [scenicRow, merchantRow, staffRow]
|
||||||
|
let infoStack = UIStackView(arrangedSubviews: infoRows)
|
||||||
infoStack.axis = .vertical
|
infoStack.axis = .vertical
|
||||||
infoCardView.addSubview(infoStack)
|
infoCardView.addSubview(infoStack)
|
||||||
|
|
||||||
voiceCardView.addSubview(voiceTitleLabel)
|
voiceCardView.addSubview(voiceTitleLabel)
|
||||||
voiceCardView.addSubview(voiceSwitch)
|
voiceCardView.addSubview(voiceSwitch)
|
||||||
|
|
||||||
qrContentStack.snp.makeConstraints { make in
|
if usesBranding {
|
||||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 32, left: 16, bottom: 32, right: 16))
|
let settingsStack = UIStackView(arrangedSubviews: [recordRow, voiceCardView])
|
||||||
|
settingsStack.axis = .vertical
|
||||||
|
settingsCardView.addSubview(settingsStack)
|
||||||
|
settingsStack.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
let divider = UIView()
|
||||||
|
divider.backgroundColor = UIColor(hex: 0xD9E2EA).withAlphaComponent(0.75)
|
||||||
|
settingsCardView.addSubview(divider)
|
||||||
|
divider.snp.makeConstraints { make in
|
||||||
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
make.height.equalTo(0.5)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
qrContentStack.snp.makeConstraints { make in
|
||||||
|
let verticalInset: CGFloat = usesBranding ? 0 : 32
|
||||||
|
make.edges.equalToSuperview().inset(
|
||||||
|
UIEdgeInsets(top: verticalInset, left: 16, bottom: verticalInset, right: 16)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
scenicNameLabel.isHidden = usesBranding
|
||||||
scenicNameLabel.snp.makeConstraints { make in
|
scenicNameLabel.snp.makeConstraints { make in
|
||||||
make.leading.trailing.equalToSuperview()
|
make.leading.trailing.equalToSuperview()
|
||||||
}
|
}
|
||||||
qrImageView.snp.makeConstraints { make in
|
if usesBranding {
|
||||||
make.width.height.equalTo(182)
|
qrContainerView.snp.makeConstraints { make in
|
||||||
|
make.width.height.equalTo(brandQRContainerSize)
|
||||||
|
}
|
||||||
|
qrImageView.snp.makeConstraints { make in
|
||||||
|
make.center.equalToSuperview()
|
||||||
|
make.width.height.equalTo(brandQRImageSize)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
qrImageView.snp.makeConstraints { make in
|
||||||
|
make.width.height.equalTo(182)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
qrPlaceholderView.snp.makeConstraints { make in
|
qrPlaceholderView.snp.makeConstraints { make in
|
||||||
make.width.height.equalTo(182)
|
make.width.height.equalTo(usesBranding ? brandQRContainerSize : 182)
|
||||||
}
|
}
|
||||||
qrErrorLabel.snp.makeConstraints { make in
|
qrErrorLabel.snp.makeConstraints { make in
|
||||||
make.top.equalToSuperview().offset(56)
|
make.top.equalToSuperview().offset(56)
|
||||||
@ -162,12 +300,36 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func setupConstraints() {
|
override func setupConstraints() {
|
||||||
|
if viewModel.usesNalatiBranding {
|
||||||
|
backgroundImageView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
scrollView.snp.makeConstraints { make in
|
scrollView.snp.makeConstraints { make in
|
||||||
make.edges.equalTo(view.safeAreaLayoutGuide)
|
if viewModel.usesNalatiBranding {
|
||||||
|
make.leading.trailing.equalToSuperview()
|
||||||
|
make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(-44)
|
||||||
|
make.bottom.equalTo(view.safeAreaLayoutGuide)
|
||||||
|
} else {
|
||||||
|
make.edges.equalTo(view.safeAreaLayoutGuide)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contentContainerView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
make.width.equalTo(scrollView.snp.width)
|
||||||
|
if viewModel.usesNalatiBranding {
|
||||||
|
make.height.equalTo(scrollView.snp.height)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
contentStack.snp.makeConstraints { make in
|
contentStack.snp.makeConstraints { make in
|
||||||
make.edges.equalToSuperview().inset(AppSpacing.screenHorizontalInset)
|
|
||||||
make.width.equalTo(scrollView.snp.width).offset(-AppSpacing.screenHorizontalInset * 2)
|
make.width.equalTo(scrollView.snp.width).offset(-AppSpacing.screenHorizontalInset * 2)
|
||||||
|
if viewModel.usesNalatiBranding {
|
||||||
|
make.centerX.centerY.equalToSuperview()
|
||||||
|
make.top.greaterThanOrEqualToSuperview()
|
||||||
|
make.bottom.lessThanOrEqualToSuperview()
|
||||||
|
} else {
|
||||||
|
make.edges.equalToSuperview().inset(AppSpacing.screenHorizontalInset)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,13 +350,30 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
applyViewModel()
|
||||||
Task { await loadData() }
|
Task { await loadData() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
applyBrandNavigationAppearanceIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
super.viewWillDisappear(animated)
|
||||||
|
restoreNavigationAppearanceIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||||
|
viewModel.usesNalatiBranding ? .darkContent : .default
|
||||||
|
}
|
||||||
|
|
||||||
private func loadData() async {
|
private func loadData() async {
|
||||||
|
async let pageConfig: Void = viewModel.loadPayPageConfig(api: paymentAPI)
|
||||||
showLoading()
|
showLoading()
|
||||||
await viewModel.loadPayCode(api: paymentAPI)
|
await viewModel.loadPayCode(api: paymentAPI)
|
||||||
hideLoading()
|
hideLoading()
|
||||||
|
await pageConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@ -203,15 +382,20 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
scenicRow.setValue(viewModel.displayScenicName)
|
scenicRow.setValue(viewModel.displayScenicName)
|
||||||
merchantRow.setValue(viewModel.merchantInfo.company)
|
merchantRow.setValue(viewModel.merchantInfo.company)
|
||||||
staffRow.setValue(viewModel.staffId.isEmpty ? "-" : viewModel.staffId)
|
staffRow.setValue(viewModel.staffId.isEmpty ? "-" : viewModel.staffId)
|
||||||
|
photographerRow.setValue(viewModel.displayPhotographerName)
|
||||||
|
storeRow.setValue(viewModel.displayStoreNames)
|
||||||
voiceSwitch.isOn = viewModel.isVoiceBroadcastOpen
|
voiceSwitch.isOn = viewModel.isVoiceBroadcastOpen
|
||||||
|
applyBrandConfigIfNeeded()
|
||||||
|
|
||||||
if let image = viewModel.qrImage {
|
if let image = viewModel.qrImage {
|
||||||
qrImageView.image = image
|
qrImageView.image = image
|
||||||
qrImageView.isHidden = false
|
qrImageView.isHidden = false
|
||||||
|
qrContainerView.isHidden = !viewModel.usesNalatiBranding
|
||||||
qrPlaceholderView.isHidden = true
|
qrPlaceholderView.isHidden = true
|
||||||
actionRow.isHidden = false
|
actionRow.isHidden = false
|
||||||
} else {
|
} else {
|
||||||
qrImageView.isHidden = true
|
qrImageView.isHidden = true
|
||||||
|
qrContainerView.isHidden = true
|
||||||
qrPlaceholderView.isHidden = false
|
qrPlaceholderView.isHidden = false
|
||||||
actionRow.isHidden = true
|
actionRow.isHidden = true
|
||||||
}
|
}
|
||||||
@ -254,6 +438,181 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
button.titleLabel?.font = .systemFont(ofSize: 14)
|
button.titleLabel?.font = .systemFont(ofSize: 14)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func configureCard(_ view: UIView, branded: Bool) {
|
||||||
|
view.backgroundColor = branded ? UIColor.white.withAlphaComponent(0.9) : .white
|
||||||
|
view.layer.cornerRadius = branded ? 18 : AppRadius.lg
|
||||||
|
view.clipsToBounds = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureBrandHeader() {
|
||||||
|
brandHeaderStack.axis = .vertical
|
||||||
|
brandHeaderStack.alignment = .center
|
||||||
|
brandHeaderStack.spacing = brandScreenHeight <= 700 ? 6 : 8
|
||||||
|
|
||||||
|
brandLogoImageView.image = UIImage(named: "payment_nalati_logo")
|
||||||
|
brandLogoImageView.contentMode = .scaleAspectFit
|
||||||
|
brandLogoImageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
|
|
||||||
|
brandTitleLabel.font = .systemFont(
|
||||||
|
ofSize: brandScreenHeight <= 700 ? 21 : 23,
|
||||||
|
weight: .bold
|
||||||
|
)
|
||||||
|
brandTitleLabel.textColor = UIColor(hex: 0x102D54)
|
||||||
|
brandTitleLabel.textAlignment = .center
|
||||||
|
brandTitleLabel.numberOfLines = 1
|
||||||
|
brandTitleLabel.adjustsFontSizeToFitWidth = true
|
||||||
|
brandTitleLabel.minimumScaleFactor = 0.8
|
||||||
|
|
||||||
|
brandSubtitleLabel.font = .systemFont(
|
||||||
|
ofSize: brandScreenHeight <= 700 ? 15 : 16,
|
||||||
|
weight: .medium
|
||||||
|
)
|
||||||
|
brandSubtitleLabel.textColor = UIColor(hex: 0x183A61)
|
||||||
|
brandSubtitleLabel.textAlignment = .center
|
||||||
|
|
||||||
|
let subtitleRow = UIStackView()
|
||||||
|
subtitleRow.axis = .horizontal
|
||||||
|
subtitleRow.alignment = .center
|
||||||
|
subtitleRow.spacing = 9
|
||||||
|
|
||||||
|
let leftLine = makeBrandLine()
|
||||||
|
let leftDiamond = makeBrandDiamond()
|
||||||
|
let rightDiamond = makeBrandDiamond()
|
||||||
|
let rightLine = makeBrandLine()
|
||||||
|
[leftLine, leftDiamond, brandSubtitleLabel, rightDiamond, rightLine].forEach(
|
||||||
|
subtitleRow.addArrangedSubview
|
||||||
|
)
|
||||||
|
|
||||||
|
brandHeaderStack.addArrangedSubview(brandLogoImageView)
|
||||||
|
brandHeaderStack.addArrangedSubview(brandTitleLabel)
|
||||||
|
brandHeaderStack.addArrangedSubview(subtitleRow)
|
||||||
|
brandHeaderStack.setCustomSpacing(
|
||||||
|
brandScreenHeight <= 700 ? 8 : 10,
|
||||||
|
after: brandLogoImageView
|
||||||
|
)
|
||||||
|
|
||||||
|
brandLogoImageView.snp.makeConstraints { make in
|
||||||
|
make.width.equalToSuperview().inset(8)
|
||||||
|
make.height.equalTo(brandLogoHeight)
|
||||||
|
}
|
||||||
|
brandTitleLabel.snp.makeConstraints { make in
|
||||||
|
make.leading.trailing.equalToSuperview().inset(8)
|
||||||
|
}
|
||||||
|
[leftLine, rightLine].forEach { line in
|
||||||
|
line.snp.makeConstraints { make in
|
||||||
|
make.width.equalTo(42)
|
||||||
|
make.height.equalTo(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[leftDiamond, rightDiamond].forEach { diamond in
|
||||||
|
diamond.snp.makeConstraints { make in
|
||||||
|
make.width.height.equalTo(6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeBrandLine() -> UIView {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor(hex: 0x27496D).withAlphaComponent(0.75)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeBrandDiamond() -> UIView {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor(hex: 0x27496D)
|
||||||
|
view.transform = CGAffineTransform(rotationAngle: .pi / 4)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func applyBrandConfigIfNeeded() {
|
||||||
|
guard viewModel.usesNalatiBranding else { return }
|
||||||
|
let config = viewModel.payPageConfig
|
||||||
|
brandTitleLabel.text = config.title
|
||||||
|
brandSubtitleLabel.text = config.subtitle
|
||||||
|
|
||||||
|
let versionChanged = appliedBrandingRefreshVersion != viewModel.brandingRefreshVersion
|
||||||
|
guard appliedBrandConfig != config || versionChanged else { return }
|
||||||
|
let forceRefresh = appliedBrandingRefreshVersion >= 0 && versionChanged
|
||||||
|
setBrandImage(
|
||||||
|
backgroundImageView,
|
||||||
|
urlString: config.background,
|
||||||
|
fallbackName: "payment_nalati_background",
|
||||||
|
forceRefresh: forceRefresh
|
||||||
|
)
|
||||||
|
setBrandImage(
|
||||||
|
brandLogoImageView,
|
||||||
|
urlString: config.logo,
|
||||||
|
fallbackName: "payment_nalati_logo",
|
||||||
|
forceRefresh: forceRefresh
|
||||||
|
)
|
||||||
|
appliedBrandConfig = config
|
||||||
|
appliedBrandingRefreshVersion = viewModel.brandingRefreshVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setBrandImage(
|
||||||
|
_ imageView: UIImageView,
|
||||||
|
urlString: String,
|
||||||
|
fallbackName: String,
|
||||||
|
forceRefresh: Bool
|
||||||
|
) {
|
||||||
|
let fallback = UIImage(named: fallbackName)
|
||||||
|
let trimmed = urlString.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard let url = URL(string: trimmed),
|
||||||
|
url.scheme?.lowercased() == "https",
|
||||||
|
url.host != nil else {
|
||||||
|
imageView.image = fallback
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var options: KingfisherOptionsInfo = [
|
||||||
|
.keepCurrentImageWhileLoading,
|
||||||
|
.cacheOriginalImage,
|
||||||
|
.transition(.fade(0.2)),
|
||||||
|
]
|
||||||
|
if forceRefresh {
|
||||||
|
options.append(.forceRefresh)
|
||||||
|
}
|
||||||
|
imageView.kf.setImage(with: url, placeholder: fallback, options: options)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyBrandNavigationAppearanceIfNeeded() {
|
||||||
|
guard viewModel.usesNalatiBranding, let navigationBar = navigationController?.navigationBar else { return }
|
||||||
|
previousStandardAppearance = navigationBar.standardAppearance
|
||||||
|
previousScrollEdgeAppearance = navigationBar.scrollEdgeAppearance
|
||||||
|
previousCompactAppearance = navigationBar.compactAppearance
|
||||||
|
previousNavigationTintColor = navigationBar.tintColor
|
||||||
|
previousNavigationTranslucency = navigationBar.isTranslucent
|
||||||
|
|
||||||
|
let appearance = UINavigationBarAppearance()
|
||||||
|
appearance.configureWithTransparentBackground()
|
||||||
|
appearance.shadowColor = .clear
|
||||||
|
appearance.titleTextAttributes = [
|
||||||
|
.foregroundColor: UIColor(hex: 0x111827),
|
||||||
|
.font: UIFont.app(.title),
|
||||||
|
]
|
||||||
|
navigationBar.standardAppearance = appearance
|
||||||
|
navigationBar.scrollEdgeAppearance = appearance
|
||||||
|
navigationBar.compactAppearance = appearance
|
||||||
|
navigationBar.tintColor = UIColor(hex: 0x111827)
|
||||||
|
navigationBar.isTranslucent = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func restoreNavigationAppearanceIfNeeded() {
|
||||||
|
guard viewModel.usesNalatiBranding, let navigationBar = navigationController?.navigationBar else { return }
|
||||||
|
if let previousStandardAppearance {
|
||||||
|
navigationBar.standardAppearance = previousStandardAppearance
|
||||||
|
}
|
||||||
|
navigationBar.scrollEdgeAppearance = previousScrollEdgeAppearance
|
||||||
|
navigationBar.compactAppearance = previousCompactAppearance
|
||||||
|
navigationBar.tintColor = previousNavigationTintColor
|
||||||
|
navigationBar.isTranslucent = previousNavigationTranslucency
|
||||||
|
previousStandardAppearance = nil
|
||||||
|
previousScrollEdgeAppearance = nil
|
||||||
|
previousCompactAppearance = nil
|
||||||
|
previousNavigationTintColor = nil
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func setAmountTapped() {
|
@objc private func setAmountTapped() {
|
||||||
viewModel.showSetAmountDialog()
|
viewModel.showSetAmountDialog()
|
||||||
}
|
}
|
||||||
@ -310,7 +669,7 @@ private final class PaymentInfoRowView: UIView {
|
|||||||
valueLabel.font = .systemFont(ofSize: 14)
|
valueLabel.font = .systemFont(ofSize: 14)
|
||||||
valueLabel.textColor = AppColor.textPrimary
|
valueLabel.textColor = AppColor.textPrimary
|
||||||
valueLabel.textAlignment = .right
|
valueLabel.textAlignment = .right
|
||||||
valueLabel.numberOfLines = 2
|
valueLabel.numberOfLines = 0
|
||||||
|
|
||||||
divider.backgroundColor = UIColor(hex: 0xE5E7EB)
|
divider.backgroundColor = UIColor(hex: 0xE5E7EB)
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,9 @@ final class TravelAlbumCodeDialogViewController: UIViewController {
|
|||||||
private let albumNameLabel = UILabel()
|
private let albumNameLabel = UILabel()
|
||||||
private let qrImageView = UIImageView()
|
private let qrImageView = UIImageView()
|
||||||
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
|
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
|
||||||
private let hintLabel = UILabel()
|
private let hintLabel = TravelAlbumCodeHintLabel(
|
||||||
|
contentInsets: UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)
|
||||||
|
)
|
||||||
private let downloadButton = UIButton(type: .system)
|
private let downloadButton = UIButton(type: .system)
|
||||||
|
|
||||||
init(state: TravelAlbumEntryViewModel.AlbumCodeState) {
|
init(state: TravelAlbumEntryViewModel.AlbumCodeState) {
|
||||||
@ -117,8 +119,8 @@ final class TravelAlbumCodeDialogViewController: UIViewController {
|
|||||||
hintLabel.snp.makeConstraints { make in
|
hintLabel.snp.makeConstraints { make in
|
||||||
make.top.equalTo(qrImageView.snp.bottom).offset(18)
|
make.top.equalTo(qrImageView.snp.bottom).offset(18)
|
||||||
make.centerX.equalToSuperview()
|
make.centerX.equalToSuperview()
|
||||||
make.height.equalTo(34)
|
|
||||||
make.leading.greaterThanOrEqualToSuperview().offset(20)
|
make.leading.greaterThanOrEqualToSuperview().offset(20)
|
||||||
|
make.trailing.lessThanOrEqualToSuperview().offset(-20)
|
||||||
}
|
}
|
||||||
downloadButton.snp.makeConstraints { make in
|
downloadButton.snp.makeConstraints { make in
|
||||||
make.top.equalTo(hintLabel.snp.bottom).offset(20)
|
make.top.equalTo(hintLabel.snp.bottom).offset(20)
|
||||||
@ -160,3 +162,30 @@ final class TravelAlbumCodeDialogViewController: UIViewController {
|
|||||||
onDownload?()
|
onDownload?()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 为相册码提示文案提供与 Android 弹窗一致的内容留白。
|
||||||
|
private final class TravelAlbumCodeHintLabel: UILabel {
|
||||||
|
private let contentInsets: UIEdgeInsets
|
||||||
|
|
||||||
|
init(contentInsets: UIEdgeInsets) {
|
||||||
|
self.contentInsets = contentInsets
|
||||||
|
super.init(frame: .zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func drawText(in rect: CGRect) {
|
||||||
|
super.drawText(in: rect.inset(by: contentInsets))
|
||||||
|
}
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
let size = super.intrinsicContentSize
|
||||||
|
return CGSize(
|
||||||
|
width: size.width + contentInsets.left + contentInsets.right,
|
||||||
|
height: size.height + contentInsets.top + contentInsets.bottom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -184,7 +184,8 @@ extension TravelAlbumEntryViewController: UITableViewDelegate {
|
|||||||
/// 新建相册任务蓝色入口卡片。
|
/// 新建相册任务蓝色入口卡片。
|
||||||
private final class TravelAlbumHeroCard: UIControl {
|
private final class TravelAlbumHeroCard: UIControl {
|
||||||
private let gradientLayer = CAGradientLayer()
|
private let gradientLayer = CAGradientLayer()
|
||||||
private let plusView = UIImageView(image: UIImage(systemName: "plus"))
|
private let plusView = UIView()
|
||||||
|
private let plusImageView = UIImageView(image: UIImage(systemName: "plus"))
|
||||||
private let titleLabel = UILabel()
|
private let titleLabel = UILabel()
|
||||||
private let subtitleLabel = UILabel()
|
private let subtitleLabel = UILabel()
|
||||||
private let arrowLabel = UILabel()
|
private let arrowLabel = UILabel()
|
||||||
@ -202,10 +203,11 @@ private final class TravelAlbumHeroCard: UIControl {
|
|||||||
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
|
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
|
||||||
layer.insertSublayer(gradientLayer, at: 0)
|
layer.insertSublayer(gradientLayer, at: 0)
|
||||||
|
|
||||||
plusView.tintColor = AppColor.primary
|
|
||||||
plusView.contentMode = .scaleAspectFit
|
|
||||||
plusView.backgroundColor = .white
|
plusView.backgroundColor = .white
|
||||||
plusView.layer.cornerRadius = 17
|
plusView.layer.cornerRadius = 17
|
||||||
|
plusView.clipsToBounds = true
|
||||||
|
plusImageView.tintColor = AppColor.primary
|
||||||
|
plusImageView.contentMode = .scaleAspectFit
|
||||||
|
|
||||||
titleLabel.text = "新建相册任务"
|
titleLabel.text = "新建相册任务"
|
||||||
titleLabel.font = .systemFont(ofSize: 19, weight: .medium)
|
titleLabel.font = .systemFont(ofSize: 19, weight: .medium)
|
||||||
@ -218,6 +220,7 @@ private final class TravelAlbumHeroCard: UIControl {
|
|||||||
arrowLabel.font = .systemFont(ofSize: 36, weight: .light)
|
arrowLabel.font = .systemFont(ofSize: 36, weight: .light)
|
||||||
|
|
||||||
addSubview(plusView)
|
addSubview(plusView)
|
||||||
|
plusView.addSubview(plusImageView)
|
||||||
addSubview(titleLabel)
|
addSubview(titleLabel)
|
||||||
addSubview(subtitleLabel)
|
addSubview(subtitleLabel)
|
||||||
addSubview(arrowLabel)
|
addSubview(arrowLabel)
|
||||||
@ -226,6 +229,10 @@ private final class TravelAlbumHeroCard: UIControl {
|
|||||||
make.centerY.equalToSuperview()
|
make.centerY.equalToSuperview()
|
||||||
make.size.equalTo(34)
|
make.size.equalTo(34)
|
||||||
}
|
}
|
||||||
|
plusImageView.snp.makeConstraints { make in
|
||||||
|
make.center.equalToSuperview()
|
||||||
|
make.size.equalTo(26)
|
||||||
|
}
|
||||||
titleLabel.snp.makeConstraints { make in
|
titleLabel.snp.makeConstraints { make in
|
||||||
make.top.equalToSuperview().offset(30)
|
make.top.equalToSuperview().offset(30)
|
||||||
make.leading.equalTo(plusView.snp.trailing).offset(16)
|
make.leading.equalTo(plusView.snp.trailing).offset(16)
|
||||||
|
|||||||
@ -22,6 +22,10 @@
|
|||||||
#if __has_include(<UMCommon/UMCommon.h>)
|
#if __has_include(<UMCommon/UMCommon.h>)
|
||||||
#import <UMCommon/UMCommon.h>
|
#import <UMCommon/UMCommon.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<JPUSHService.h>)
|
||||||
|
#import <JPUSHService.h>
|
||||||
|
#endif
|
||||||
#import "Features/ScenicQueue/Services/AliyunNuiTTSBridge.h"
|
#import "Features/ScenicQueue/Services/AliyunNuiTTSBridge.h"
|
||||||
|
|
||||||
#if __has_include(<UMAPM/UMAPM.h>)
|
#if __has_include(<UMAPM/UMAPM.h>)
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>$(APS_ENVIRONMENT)</string>
|
||||||
<key>com.apple.developer.associated-domains</key>
|
<key>com.apple.developer.associated-domains</key>
|
||||||
<array>
|
<array>
|
||||||
<string>applinks:www.zhifly.cn</string>
|
<string>applinks:www.zhifly.cn</string>
|
||||||
|
|||||||
@ -29,7 +29,7 @@ final class AllFunctionsViewModelTests: XCTestCase {
|
|||||||
super.tearDown()
|
super.tearDown()
|
||||||
}
|
}
|
||||||
|
|
||||||
func testLoadFunctionsUsesDefaultFirstFourWhenNoSavedURIs() {
|
func testLoadFunctionsUsesDefaultFirstFourAndExcludesMessageCenter() {
|
||||||
appStore.permissions.savePermissionItems([
|
appStore.permissions.savePermissionItems([
|
||||||
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
|
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
|
||||||
HomePermissionItem(id: "2", name: "云盘", uri: "cloud_management"),
|
HomePermissionItem(id: "2", name: "云盘", uri: "cloud_management"),
|
||||||
@ -44,7 +44,7 @@ final class AllFunctionsViewModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(viewModel.commonMenus.map(\.uri), [
|
XCTAssertEqual(viewModel.commonMenus.map(\.uri), [
|
||||||
"wallet", "cloud_management", "task_management", "system_settings",
|
"wallet", "cloud_management", "task_management", "system_settings",
|
||||||
])
|
])
|
||||||
XCTAssertEqual(viewModel.moreMenus.map(\.uri), ["message_center"])
|
XCTAssertTrue(viewModel.moreMenus.isEmpty)
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
menuStore.savedCommonURIs(
|
menuStore.savedCommonURIs(
|
||||||
accountScope: appStore.session.accountCachePrefix,
|
accountScope: appStore.session.accountCachePrefix,
|
||||||
|
|||||||
@ -23,14 +23,26 @@ final class HomeCommonMenuStoreTests: XCTestCase {
|
|||||||
super.tearDown()
|
super.tearDown()
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDefaultCommonURIsTakeFirstFour() {
|
func testDefaultCommonURIsTakeFirstFourAndExcludeMessageCenter() {
|
||||||
let permissions = (1...6).map {
|
let permissions = [
|
||||||
|
HomePermissionItem(id: "0", name: "消息", uri: "message_center"),
|
||||||
|
] + (1...5).map {
|
||||||
HomePermissionItem(id: "\($0)", name: "菜单\($0)", uri: "uri_\($0)")
|
HomePermissionItem(id: "\($0)", name: "菜单\($0)", uri: "uri_\($0)")
|
||||||
}
|
}
|
||||||
let uris = HomeCommonMenuStore.defaultCommonURIs(from: permissions)
|
let uris = HomeCommonMenuStore.defaultCommonURIs(from: permissions)
|
||||||
XCTAssertEqual(uris, ["uri_1", "uri_2", "uri_3", "uri_4"])
|
XCTAssertEqual(uris, ["uri_1", "uri_2", "uri_3", "uri_4"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testSavedMessageCenterIsRemovedFromLegacyCommonURIs() {
|
||||||
|
let key = "scenic_user_u1_role_photographer_common_uris"
|
||||||
|
defaults.set(["message_center", "wallet"], forKey: key)
|
||||||
|
|
||||||
|
let uris = store.savedCommonURIs(accountScope: "scenic_user_u1", roleCode: "photographer")
|
||||||
|
|
||||||
|
XCTAssertEqual(uris, ["wallet"])
|
||||||
|
XCTAssertEqual(defaults.stringArray(forKey: key), ["wallet"])
|
||||||
|
}
|
||||||
|
|
||||||
func testSavedCommonURIsOverrideDefault() {
|
func testSavedCommonURIsOverrideDefault() {
|
||||||
let permissions = [
|
let permissions = [
|
||||||
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
|
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
|
||||||
@ -65,8 +77,12 @@ final class HomeCommonMenuStoreTests: XCTestCase {
|
|||||||
HomeMenuItem(uri: "wallet", title: "钱包", iconName: "wallet.pass"),
|
HomeMenuItem(uri: "wallet", title: "钱包", iconName: "wallet.pass"),
|
||||||
HomeMenuItem(uri: "cloud_management", title: "云盘", iconName: "icloud"),
|
HomeMenuItem(uri: "cloud_management", title: "云盘", iconName: "icloud"),
|
||||||
HomeMenuItem(uri: "task_management", title: "任务", iconName: "checklist"),
|
HomeMenuItem(uri: "task_management", title: "任务", iconName: "checklist"),
|
||||||
|
HomeMenuItem(uri: "message_center", title: "消息中心", iconName: "bell"),
|
||||||
]
|
]
|
||||||
let split = HomeCommonMenuStore.splitMenus(from: menus, commonURIs: ["wallet", "task_management"])
|
let split = HomeCommonMenuStore.splitMenus(
|
||||||
|
from: menus,
|
||||||
|
commonURIs: ["wallet", "task_management", "message_center"]
|
||||||
|
)
|
||||||
XCTAssertEqual(split.common.map(\.uri), ["wallet", "task_management"])
|
XCTAssertEqual(split.common.map(\.uri), ["wallet", "task_management"])
|
||||||
XCTAssertEqual(split.more.map(\.uri), ["cloud_management"])
|
XCTAssertEqual(split.more.map(\.uri), ["cloud_management"])
|
||||||
}
|
}
|
||||||
@ -75,7 +91,7 @@ final class HomeCommonMenuStoreTests: XCTestCase {
|
|||||||
let item = HomeMenuCatalog.item(for: "report_photographer")
|
let item = HomeMenuCatalog.item(for: "report_photographer")
|
||||||
|
|
||||||
XCTAssertEqual(item?.title, "举报摄影师")
|
XCTAssertEqual(item?.title, "举报摄影师")
|
||||||
XCTAssertEqual(item?.iconName, "shield.fill")
|
XCTAssertEqual(item?.iconName, "shield.lefthalf.filled")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testHomeMenuCatalogUsesIOS16CompatibleIcons() {
|
func testHomeMenuCatalogUsesIOS16CompatibleIcons() {
|
||||||
|
|||||||
@ -90,10 +90,10 @@ final class HomeViewModelTests: XCTestCase {
|
|||||||
let menuStore = HomeCommonMenuStore(defaults: defaults)
|
let menuStore = HomeCommonMenuStore(defaults: defaults)
|
||||||
appStore.permissions.savePermissionItems([
|
appStore.permissions.savePermissionItems([
|
||||||
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
|
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
|
||||||
HomePermissionItem(id: "2", name: "云盘", uri: "cloud_management"),
|
HomePermissionItem(id: "2", name: "消息", uri: "message_center"),
|
||||||
HomePermissionItem(id: "3", name: "任务", uri: "task_management"),
|
HomePermissionItem(id: "3", name: "云盘", uri: "cloud_management"),
|
||||||
HomePermissionItem(id: "4", name: "设置", uri: "system_settings"),
|
HomePermissionItem(id: "4", name: "任务", uri: "task_management"),
|
||||||
HomePermissionItem(id: "5", name: "消息", uri: "message_center"),
|
HomePermissionItem(id: "5", name: "设置", uri: "system_settings"),
|
||||||
])
|
])
|
||||||
let viewModel = HomeViewModel(appStore: appStore, commonMenuStore: menuStore)
|
let viewModel = HomeViewModel(appStore: appStore, commonMenuStore: menuStore)
|
||||||
|
|
||||||
@ -231,6 +231,50 @@ final class HomeViewModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(session.requests.count, 1)
|
XCTAssertEqual(session.requests.count, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testUnreadMessageStatusUsesCountEndpointAndUpdatesBadgeState() async {
|
||||||
|
let api = HomeMessageCenterAPIMock()
|
||||||
|
api.unreadCountResponses = [
|
||||||
|
MessageUnreadCountResponse(unreadCount: 9),
|
||||||
|
MessageUnreadCountResponse(unreadCount: 0),
|
||||||
|
]
|
||||||
|
let viewModel = HomeViewModel(appStore: appStore)
|
||||||
|
|
||||||
|
await viewModel.refreshUnreadMessageStatus(api: api)
|
||||||
|
XCTAssertTrue(viewModel.hasUnreadMessages)
|
||||||
|
XCTAssertEqual(viewModel.unreadMessageCount, 9)
|
||||||
|
|
||||||
|
await viewModel.refreshUnreadMessageStatus(api: api)
|
||||||
|
XCTAssertFalse(viewModel.hasUnreadMessages)
|
||||||
|
XCTAssertEqual(viewModel.unreadMessageCount, 0)
|
||||||
|
XCTAssertEqual(api.unreadCountCallCount, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScenicHeaderMessageButtonShowsUnreadStateAndHandlesTap() throws {
|
||||||
|
let headerView = HomeScenicHeaderView(frame: CGRect(x: 0, y: 0, width: 390, height: 100))
|
||||||
|
var tapCount = 0
|
||||||
|
headerView.onMessageTap = { tapCount += 1 }
|
||||||
|
|
||||||
|
headerView.apply(scenicName: "测试景区", hasUnreadMessages: true)
|
||||||
|
headerView.layoutIfNeeded()
|
||||||
|
|
||||||
|
let button = try XCTUnwrap(
|
||||||
|
findSubview(in: headerView) { $0.accessibilityIdentifier == "home_message_button" } as? UIButton
|
||||||
|
)
|
||||||
|
let unreadDot = try XCTUnwrap(
|
||||||
|
findSubview(in: headerView) { $0.accessibilityIdentifier == "home_message_unread_dot" }
|
||||||
|
)
|
||||||
|
XCTAssertEqual(button.bounds.size, CGSize(width: 44, height: 44))
|
||||||
|
XCTAssertEqual(button.accessibilityLabel, "消息中心,有未读消息")
|
||||||
|
XCTAssertFalse(unreadDot.isHidden)
|
||||||
|
|
||||||
|
button.sendActions(for: .touchUpInside)
|
||||||
|
XCTAssertEqual(tapCount, 1)
|
||||||
|
|
||||||
|
headerView.apply(scenicName: "测试景区", hasUnreadMessages: false)
|
||||||
|
XCTAssertEqual(button.accessibilityLabel, "消息中心")
|
||||||
|
XCTAssertTrue(unreadDot.isHidden)
|
||||||
|
}
|
||||||
|
|
||||||
func testRefreshLocalDisplayStateClearsWhenNameMissing() {
|
func testRefreshLocalDisplayStateClearsWhenNameMissing() {
|
||||||
appStore.session.currentScenicId = 10
|
appStore.session.currentScenicId = 10
|
||||||
appStore.session.currentScenicName = ""
|
appStore.session.currentScenicName = ""
|
||||||
@ -283,6 +327,45 @@ final class HomeViewModelTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func findSubview(in view: UIView, matching predicate: (UIView) -> Bool) -> UIView? {
|
||||||
|
if predicate(view) {
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
for subview in view.subviews {
|
||||||
|
if let match = findSubview(in: subview, matching: predicate) {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 首页未读消息查询替身。
|
||||||
|
@MainActor
|
||||||
|
private final class HomeMessageCenterAPIMock: MessageCenterServing {
|
||||||
|
var unreadCountResponses: [MessageUnreadCountResponse] = []
|
||||||
|
var unreadCountCallCount = 0
|
||||||
|
|
||||||
|
func list(lastId: Int, limit: Int, unread: Int) async throws -> MessageListResponse {
|
||||||
|
MessageListResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unreadCount() async throws -> MessageUnreadCountResponse {
|
||||||
|
unreadCountCallCount += 1
|
||||||
|
guard !unreadCountResponses.isEmpty else { return MessageUnreadCountResponse() }
|
||||||
|
return unreadCountResponses.removeFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
func markAsRead(messageId: Int) async throws -> MessageUnreadCountResponse {
|
||||||
|
MessageUnreadCountResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
func markAllAsRead() async throws -> MessageReadAllResponse {
|
||||||
|
MessageReadAllResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
func delete(messageId: Int) async throws {}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension UIColor {
|
private extension UIColor {
|
||||||
|
|||||||
@ -53,21 +53,37 @@ final class MessageCenterAPITests: XCTestCase {
|
|||||||
XCTAssertEqual(response.items.first?.extraData?["order_no"], .string("NO123"))
|
XCTAssertEqual(response.items.first?.extraData?["order_no"], .string("NO123"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testReadAndDeleteUseAndroidPathsAndBody() async throws {
|
func testUnreadCountReadAllReadAndDeleteUseExpectedContracts() async throws {
|
||||||
let session = MockURLSession(responses: [envelopeJSON("{}"), envelopeJSON("{}")])
|
let session = MockURLSession(responses: [
|
||||||
|
envelopeJSON(#"{"unread_count":"12"}"#),
|
||||||
|
envelopeJSON(#"{"unread_count":11}"#),
|
||||||
|
envelopeJSON(#"{"updated_count":11,"unread_count":0}"#),
|
||||||
|
envelopeJSON("{}"),
|
||||||
|
])
|
||||||
let api = MessageCenterAPI(client: APIClient(environment: .testing, session: session))
|
let api = MessageCenterAPI(client: APIClient(environment: .testing, session: session))
|
||||||
|
|
||||||
try await api.markAsRead(messageId: 21)
|
let unread = try await api.unreadCount()
|
||||||
|
let read = try await api.markAsRead(messageId: 21)
|
||||||
|
let readAll = try await api.markAllAsRead()
|
||||||
try await api.delete(messageId: 21)
|
try await api.delete(messageId: 21)
|
||||||
|
|
||||||
XCTAssertEqual(session.requests[0].httpMethod, "POST")
|
XCTAssertEqual(unread.unreadCount, 12)
|
||||||
XCTAssertEqual(session.requests[0].url?.path, "/api/app/msg/read")
|
XCTAssertEqual(session.requests[0].httpMethod, "GET")
|
||||||
let readQuery = URLComponents(url: try XCTUnwrap(session.requests[0].url), resolvingAgainstBaseURL: false)?.queryItems
|
XCTAssertEqual(session.requests[0].url?.path, "/api/app/msg/unread-count")
|
||||||
|
|
||||||
|
XCTAssertEqual(read.unreadCount, 11)
|
||||||
|
XCTAssertEqual(session.requests[1].httpMethod, "POST")
|
||||||
|
XCTAssertEqual(session.requests[1].url?.path, "/api/app/msg/read")
|
||||||
|
let readQuery = URLComponents(url: try XCTUnwrap(session.requests[1].url), resolvingAgainstBaseURL: false)?.queryItems
|
||||||
XCTAssertEqual(readQuery?.first { $0.name == "id" }?.value, "21")
|
XCTAssertEqual(readQuery?.first { $0.name == "id" }?.value, "21")
|
||||||
|
|
||||||
XCTAssertEqual(session.requests[1].httpMethod, "POST")
|
XCTAssertEqual(readAll, MessageReadAllResponse(updatedCount: 11, unreadCount: 0))
|
||||||
XCTAssertEqual(session.requests[1].url?.path, "/api/app/msg/delete")
|
XCTAssertEqual(session.requests[2].httpMethod, "POST")
|
||||||
let deleteBody = try bodyJSON(session.requests[1])
|
XCTAssertEqual(session.requests[2].url?.path, "/api/app/msg/read-all")
|
||||||
|
|
||||||
|
XCTAssertEqual(session.requests[3].httpMethod, "POST")
|
||||||
|
XCTAssertEqual(session.requests[3].url?.path, "/api/app/msg/delete")
|
||||||
|
let deleteBody = try bodyJSON(session.requests[3])
|
||||||
XCTAssertEqual(deleteBody["id"] as? Int, 21)
|
XCTAssertEqual(deleteBody["id"] as? Int, 21)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,17 +32,77 @@ final class MessageCenterViewModelTests: XCTestCase {
|
|||||||
func testUnreadSelectionMarksReadAndOpensDetail() async {
|
func testUnreadSelectionMarksReadAndOpensDetail() async {
|
||||||
let api = MockMessageCenterAPI()
|
let api = MockMessageCenterAPI()
|
||||||
api.listResponses = [MessageListResponse(hasMore: false, lastId: 0, items: [makeMessage(id: 3, isRead: false)])]
|
api.listResponses = [MessageListResponse(hasMore: false, lastId: 0, items: [makeMessage(id: 3, isRead: false)])]
|
||||||
|
api.readResponse = MessageUnreadCountResponse(unreadCount: 4)
|
||||||
let viewModel = MessageCenterViewModel()
|
let viewModel = MessageCenterViewModel()
|
||||||
var opened: MessageItem?
|
var opened: MessageItem?
|
||||||
|
var callbackCount: Int?
|
||||||
viewModel.onOpenDetail = { opened = $0 }
|
viewModel.onOpenDetail = { opened = $0 }
|
||||||
|
viewModel.onUnreadMessageCountChange = { callbackCount = $0 }
|
||||||
|
let unreadChanged = expectation(
|
||||||
|
forNotification: NotificationName.unreadMessageCountDidChange,
|
||||||
|
object: nil,
|
||||||
|
handler: { notification in
|
||||||
|
notification.userInfo?[NotificationUserInfoKey.unreadCount] as? Int == 4
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
await viewModel.loadInitial(api: api)
|
await viewModel.loadInitial(api: api)
|
||||||
await viewModel.selectMessage(id: 3, api: api)
|
await viewModel.selectMessage(id: 3, api: api)
|
||||||
|
await fulfillment(of: [unreadChanged], timeout: 1)
|
||||||
|
|
||||||
XCTAssertEqual(api.readCalls, [3])
|
XCTAssertEqual(api.readCalls, [3])
|
||||||
XCTAssertEqual(opened?.id, 3)
|
XCTAssertEqual(opened?.id, 3)
|
||||||
XCTAssertEqual(opened?.isRead, true)
|
XCTAssertEqual(opened?.isRead, true)
|
||||||
XCTAssertEqual(viewModel.items.first?.isRead, true)
|
XCTAssertEqual(viewModel.items.first?.isRead, true)
|
||||||
|
XCTAssertEqual(callbackCount, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMarkAllAsReadUpdatesItemsAndPublishesLatestCount() async {
|
||||||
|
let api = MockMessageCenterAPI()
|
||||||
|
api.listResponses = [MessageListResponse(items: [
|
||||||
|
makeMessage(id: 1, isRead: false),
|
||||||
|
makeMessage(id: 2, isRead: true),
|
||||||
|
])]
|
||||||
|
api.readAllResponse = MessageReadAllResponse(updatedCount: 1, unreadCount: 0)
|
||||||
|
let viewModel = MessageCenterViewModel()
|
||||||
|
var callbackCount: Int?
|
||||||
|
var message: String?
|
||||||
|
viewModel.onUnreadMessageCountChange = { callbackCount = $0 }
|
||||||
|
viewModel.onShowMessage = { message = $0 }
|
||||||
|
let unreadChanged = expectation(
|
||||||
|
forNotification: NotificationName.unreadMessageCountDidChange,
|
||||||
|
object: nil,
|
||||||
|
handler: { notification in
|
||||||
|
notification.userInfo?[NotificationUserInfoKey.unreadCount] as? Int == 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await viewModel.loadInitial(api: api)
|
||||||
|
let response = await viewModel.markAllAsRead(api: api)
|
||||||
|
await fulfillment(of: [unreadChanged], timeout: 1)
|
||||||
|
|
||||||
|
XCTAssertEqual(api.readAllCallCount, 1)
|
||||||
|
XCTAssertEqual(response, MessageReadAllResponse(updatedCount: 1, unreadCount: 0))
|
||||||
|
XCTAssertTrue(viewModel.items.allSatisfy(\.isRead))
|
||||||
|
XCTAssertEqual(callbackCount, 0)
|
||||||
|
XCTAssertEqual(message, "已全部标记为已读")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMarkAllAsReadFailureKeepsUnreadState() async {
|
||||||
|
let api = MockMessageCenterAPI()
|
||||||
|
api.listResponses = [MessageListResponse(items: [makeMessage(id: 6, isRead: false)])]
|
||||||
|
api.readAllError = TestError(message: "fail")
|
||||||
|
let viewModel = MessageCenterViewModel()
|
||||||
|
var message: String?
|
||||||
|
viewModel.onShowMessage = { message = $0 }
|
||||||
|
|
||||||
|
await viewModel.loadInitial(api: api)
|
||||||
|
let response = await viewModel.markAllAsRead(api: api)
|
||||||
|
|
||||||
|
XCTAssertNil(response)
|
||||||
|
XCTAssertEqual(api.readAllCallCount, 1)
|
||||||
|
XCTAssertEqual(viewModel.items.first?.isRead, false)
|
||||||
|
XCTAssertEqual(message, "全部标记已读失败,请稍后重试")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testReadSelectionOpensDetailWithoutCallingReadAPI() async {
|
func testReadSelectionOpensDetailWithoutCallingReadAPI() async {
|
||||||
@ -125,8 +185,12 @@ private final class MockMessageCenterAPI: MessageCenterServing {
|
|||||||
var listResponses: [MessageListResponse] = []
|
var listResponses: [MessageListResponse] = []
|
||||||
var listCalls: [(lastId: Int, limit: Int, unread: Int)] = []
|
var listCalls: [(lastId: Int, limit: Int, unread: Int)] = []
|
||||||
var readCalls: [Int] = []
|
var readCalls: [Int] = []
|
||||||
|
var readResponse = MessageUnreadCountResponse()
|
||||||
|
var readAllResponse = MessageReadAllResponse()
|
||||||
|
var readAllCallCount = 0
|
||||||
var deleteCalls: [Int] = []
|
var deleteCalls: [Int] = []
|
||||||
var readError: Error?
|
var readError: Error?
|
||||||
|
var readAllError: Error?
|
||||||
var deleteError: Error?
|
var deleteError: Error?
|
||||||
|
|
||||||
func list(lastId: Int, limit: Int, unread: Int) async throws -> MessageListResponse {
|
func list(lastId: Int, limit: Int, unread: Int) async throws -> MessageListResponse {
|
||||||
@ -135,9 +199,20 @@ private final class MockMessageCenterAPI: MessageCenterServing {
|
|||||||
return listResponses.removeFirst()
|
return listResponses.removeFirst()
|
||||||
}
|
}
|
||||||
|
|
||||||
func markAsRead(messageId: Int) async throws {
|
func unreadCount() async throws -> MessageUnreadCountResponse {
|
||||||
|
MessageUnreadCountResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
func markAsRead(messageId: Int) async throws -> MessageUnreadCountResponse {
|
||||||
readCalls.append(messageId)
|
readCalls.append(messageId)
|
||||||
if let readError { throw readError }
|
if let readError { throw readError }
|
||||||
|
return readResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func markAllAsRead() async throws -> MessageReadAllResponse {
|
||||||
|
readAllCallCount += 1
|
||||||
|
if let readAllError { throw readAllError }
|
||||||
|
return readAllResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
func delete(messageId: Int) async throws {
|
func delete(messageId: Int) async throws {
|
||||||
|
|||||||
@ -79,6 +79,24 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase {
|
|||||||
XCTAssertTrue(viewModel.isVoiceBroadcastOpen)
|
XCTAssertTrue(viewModel.isVoiceBroadcastOpen)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testNalatiAccountInfoUsesLoggedInPhotographerAndAllPermissionStores() {
|
||||||
|
appStore.session.userName = "摄影师小王"
|
||||||
|
appStore.permissions.saveRolePermissionList([
|
||||||
|
RolePermissionResponse(
|
||||||
|
role: RoleInfo(id: 41, name: "摄影师", roleCode: "photographer"),
|
||||||
|
store: [
|
||||||
|
RolePermissionStoreInfo(id: 1, name: "空中草原店"),
|
||||||
|
RolePermissionStoreInfo(id: 2, name: "河谷草原店"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
|
||||||
|
|
||||||
|
XCTAssertEqual(viewModel.displayPhotographerName, "摄影师小王")
|
||||||
|
XCTAssertEqual(viewModel.displayStoreNames, "空中草原店、河谷草原店")
|
||||||
|
}
|
||||||
|
|
||||||
func testLoadPayCodeClearsQRWhenScenicMissing() async {
|
func testLoadPayCodeClearsQRWhenScenicMissing() async {
|
||||||
appStore.session.currentScenicId = 0
|
appStore.session.currentScenicId = 0
|
||||||
let session = MockURLSession(responses: [])
|
let session = MockURLSession(responses: [])
|
||||||
@ -90,4 +108,74 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase {
|
|||||||
XCTAssertFalse(viewModel.hasPayCode)
|
XCTAssertFalse(viewModel.hasPayCode)
|
||||||
XCTAssertEqual(session.requests.count, 0)
|
XCTAssertEqual(session.requests.count, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testNalatiLoadsCachedConfigBeforeRefreshingRemoteConfig() async throws {
|
||||||
|
let defaults = UserDefaults(suiteName: "PaymentCollectionDetailsViewModelTests.Config")!
|
||||||
|
defaults.removePersistentDomain(forName: "PaymentCollectionDetailsViewModelTests.Config")
|
||||||
|
let cache = PayPageConfigCache(defaults: defaults, environment: .testing)
|
||||||
|
let cached = PayPageConfig(
|
||||||
|
background: "https://cache.example.com/background.png",
|
||||||
|
logo: "https://cache.example.com/logo.png",
|
||||||
|
title: "缓存标题",
|
||||||
|
subtitle: "缓存副标题"
|
||||||
|
)
|
||||||
|
cache.save(cached, scenicId: 128)
|
||||||
|
appStore.session.currentScenicId = 128
|
||||||
|
appStore.session.currentScenicName = "那拉提景区"
|
||||||
|
|
||||||
|
let responseJSON = """
|
||||||
|
{"code":100000,"msg":"success","data":{
|
||||||
|
"background":"https://new.example.com/background.png",
|
||||||
|
"logo":"",
|
||||||
|
"title":"最新标题",
|
||||||
|
"subtitle":""
|
||||||
|
}}
|
||||||
|
""".data(using: .utf8)!
|
||||||
|
let session = MockURLSession(responses: [responseJSON])
|
||||||
|
let api = PaymentAPI(client: APIClient(environment: .testing, session: session))
|
||||||
|
let viewModel = PaymentCollectionDetailsViewModel(
|
||||||
|
appStore: appStore,
|
||||||
|
payPageConfigCache: cache
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(viewModel.payPageConfig, cached)
|
||||||
|
await viewModel.loadPayPageConfig(api: api)
|
||||||
|
|
||||||
|
XCTAssertEqual(viewModel.payPageConfig.background, "https://new.example.com/background.png")
|
||||||
|
XCTAssertEqual(viewModel.payPageConfig.logo, cached.logo)
|
||||||
|
XCTAssertEqual(viewModel.payPageConfig.title, "最新标题")
|
||||||
|
XCTAssertEqual(viewModel.payPageConfig.subtitle, cached.subtitle)
|
||||||
|
XCTAssertEqual(viewModel.brandingRefreshVersion, 1)
|
||||||
|
XCTAssertEqual(session.requests.first?.url?.path, "/api/yf-handset-app/photog/pay-page-config")
|
||||||
|
XCTAssertEqual(
|
||||||
|
URLComponents(url: try XCTUnwrap(session.requests.first?.url), resolvingAgainstBaseURL: false)?
|
||||||
|
.queryItems?
|
||||||
|
.first(where: { $0.name == "scenic_id" })?
|
||||||
|
.value,
|
||||||
|
"128"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNonNalatiDoesNotRequestPayPageConfig() async {
|
||||||
|
let session = MockURLSession(responses: [])
|
||||||
|
let api = PaymentAPI(client: APIClient(environment: .testing, session: session))
|
||||||
|
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
|
||||||
|
|
||||||
|
await viewModel.loadPayPageConfig(api: api)
|
||||||
|
|
||||||
|
XCTAssertFalse(viewModel.usesNalatiBranding)
|
||||||
|
XCTAssertEqual(session.requests.count, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPayPageConfigCacheIsEnvironmentScoped() {
|
||||||
|
let defaults = UserDefaults(suiteName: "PaymentCollectionDetailsViewModelTests.Environment")!
|
||||||
|
defaults.removePersistentDomain(forName: "PaymentCollectionDetailsViewModelTests.Environment")
|
||||||
|
let production = PayPageConfigCache(defaults: defaults, environment: .production)
|
||||||
|
let testing = PayPageConfigCache(defaults: defaults, environment: .testing)
|
||||||
|
|
||||||
|
production.save(.nalatiDefault, scenicId: 128)
|
||||||
|
|
||||||
|
XCTAssertEqual(production.config(scenicId: 128), .nalatiDefault)
|
||||||
|
XCTAssertNil(testing.config(scenicId: 128))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,88 @@ import XCTest
|
|||||||
|
|
||||||
/// 收款模型解码测试。
|
/// 收款模型解码测试。
|
||||||
final class PaymentModelsTests: XCTestCase {
|
final class PaymentModelsTests: XCTestCase {
|
||||||
|
func testPayPageConfigResponseDecodesBrandFields() throws {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"background": "https://example.com/background.png",
|
||||||
|
"logo": "https://example.com/logo.png",
|
||||||
|
"title": "那拉提旅拍管理收费平台",
|
||||||
|
"subtitle": "那拉提景区"
|
||||||
|
}
|
||||||
|
""".data(using: .utf8)!
|
||||||
|
|
||||||
|
let response = try JSONDecoder().decode(PayPageConfigResponse.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertEqual(response.background, "https://example.com/background.png")
|
||||||
|
XCTAssertEqual(response.logo, "https://example.com/logo.png")
|
||||||
|
XCTAssertEqual(response.title, "那拉提旅拍管理收费平台")
|
||||||
|
XCTAssertEqual(response.subtitle, "那拉提景区")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPayPageConfigMergesOnlyUsableRemoteFields() {
|
||||||
|
let cached = PayPageConfig(
|
||||||
|
background: "https://cache.example.com/background.png",
|
||||||
|
logo: "https://cache.example.com/logo.png",
|
||||||
|
title: "缓存标题",
|
||||||
|
subtitle: "缓存副标题"
|
||||||
|
)
|
||||||
|
|
||||||
|
let merged = cached.merging(
|
||||||
|
PayPageConfigResponse(
|
||||||
|
background: "",
|
||||||
|
logo: "http://unsafe.example.com/logo.png",
|
||||||
|
title: " 最新标题 ",
|
||||||
|
subtitle: ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(merged.background, cached.background)
|
||||||
|
XCTAssertEqual(merged.logo, cached.logo)
|
||||||
|
XCTAssertEqual(merged.title, "最新标题")
|
||||||
|
XCTAssertEqual(merged.subtitle, cached.subtitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPayPageBrandingOnlyEnablesNalatiScenicId() {
|
||||||
|
XCTAssertTrue(PayPageBrandingPolicy.isNalati(scenicId: 128))
|
||||||
|
XCTAssertFalse(PayPageBrandingPolicy.isNalati(scenicId: 127))
|
||||||
|
XCTAssertFalse(PayPageBrandingPolicy.isNalati(scenicId: 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPaymentAccountDisplayFormatterUsesCurrentPhotographerAndAllUniqueStores() {
|
||||||
|
XCTAssertEqual(
|
||||||
|
PaymentAccountDisplayFormatter.photographerName(userName: " 当前摄影师 ", realName: "实名"),
|
||||||
|
"当前摄影师"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
PaymentAccountDisplayFormatter.storeNames(["一号店", " 二号店 ", "一号店", ""]),
|
||||||
|
"一号店、二号店"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(PaymentAccountDisplayFormatter.storeNames([]), "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRolePermissionResponseDecodesAllAccountStores() throws {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"role": {
|
||||||
|
"id": 41,
|
||||||
|
"name": "摄影师",
|
||||||
|
"role_code": "photographer",
|
||||||
|
"notes": null,
|
||||||
|
"permission": []
|
||||||
|
},
|
||||||
|
"scenic": [],
|
||||||
|
"store": [
|
||||||
|
{"id": 37, "name": "随心旅拍", "status": 1},
|
||||||
|
{"id": 38, "name": "空中草原店", "status": 1}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".data(using: .utf8)!
|
||||||
|
|
||||||
|
let response = try JSONDecoder().decode(RolePermissionResponse.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertEqual(response.store.map(\.name), ["随心旅拍", "空中草原店"])
|
||||||
|
}
|
||||||
|
|
||||||
func testPayCodeResponseDecodesSnakeCaseFields() throws {
|
func testPayCodeResponseDecodesSnakeCaseFields() throws {
|
||||||
let json = """
|
let json = """
|
||||||
{
|
{
|
||||||
|
|||||||
357
suixinkanTests/PushNotificationTests.swift
Normal file
357
suixinkanTests/PushNotificationTests.swift
Normal file
@ -0,0 +1,357 @@
|
|||||||
|
import UIKit
|
||||||
|
import XCTest
|
||||||
|
@testable import suixinkan
|
||||||
|
|
||||||
|
/// 极光推送测试,覆盖 payload、Registration ID 上报、生命周期和冷启动路由。
|
||||||
|
@MainActor
|
||||||
|
final class PushNotificationTests: XCTestCase {
|
||||||
|
private var defaults: UserDefaults!
|
||||||
|
private var appStore: AppStore!
|
||||||
|
private var suiteName: String!
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
super.setUp()
|
||||||
|
suiteName = "PushNotificationTests.\(UUID().uuidString)"
|
||||||
|
defaults = UserDefaults(suiteName: suiteName)!
|
||||||
|
defaults.removePersistentDomain(forName: suiteName)
|
||||||
|
appStore = AppStore(defaults: defaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDown() {
|
||||||
|
if let suiteName {
|
||||||
|
defaults.removePersistentDomain(forName: suiteName)
|
||||||
|
}
|
||||||
|
defaults = nil
|
||||||
|
appStore = nil
|
||||||
|
suiteName = nil
|
||||||
|
super.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPayloadRoutesPaymentTypesToTheirPages() {
|
||||||
|
XCTAssertEqual(
|
||||||
|
PushPayload(userInfo: ["type": 1, "route": "message_center"]).destination,
|
||||||
|
.paymentRecord
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
PushPayload(userInfo: ["type": "6", "action": "task_management"]).destination,
|
||||||
|
.paymentDetails
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPayloadIgnoresNonTypeRoutingFieldsAndOtherTypesFallBackToMessageCenter() {
|
||||||
|
let conflictingFields: [AnyHashable: Any] = [
|
||||||
|
"type": 2,
|
||||||
|
"route": "payment",
|
||||||
|
"uri": "/payment_qr",
|
||||||
|
"action": "收款",
|
||||||
|
]
|
||||||
|
|
||||||
|
[0, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 100, 999].forEach { type in
|
||||||
|
XCTAssertEqual(PushPayload(userInfo: ["type": type]).destination, .messageCenter)
|
||||||
|
}
|
||||||
|
XCTAssertEqual(PushPayload(userInfo: conflictingFields).destination, .messageCenter)
|
||||||
|
XCTAssertEqual(PushPayload(userInfo: ["route": "payment"]).destination, .messageCenter)
|
||||||
|
XCTAssertEqual(PushPayload(userInfo: ["data": ["type": 6]]).destination, .messageCenter)
|
||||||
|
XCTAssertEqual(PushPayload(userInfo: ["type": 999]).destination, .messageCenter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPayloadReadsTypeFromNestedJSONStringWithoutUsingOtherFields() {
|
||||||
|
let nested = PushPayload(userInfo: [
|
||||||
|
"extras": #"{"type":6,"route":"message_center"}"#,
|
||||||
|
])
|
||||||
|
|
||||||
|
XCTAssertEqual(nested.destination, .paymentDetails)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPushAPIUsesAndroidCompatibleEndpointAndQuery() async throws {
|
||||||
|
let session = MockURLSession(responses: [try TestJSON.envelope(data: EmptyPayload())])
|
||||||
|
let api = PushAPI(client: APIClient(environment: .testing, session: session))
|
||||||
|
|
||||||
|
try await api.registerJPushID("registration-id-123")
|
||||||
|
|
||||||
|
let request = try XCTUnwrap(session.requests.first)
|
||||||
|
XCTAssertEqual(request.httpMethod, "POST")
|
||||||
|
XCTAssertEqual(request.url?.path, "/api/app/user/register-jpush-id")
|
||||||
|
let query = URLComponents(url: try XCTUnwrap(request.url), resolvingAgainstBaseURL: false)?.queryItems
|
||||||
|
XCTAssertEqual(query?.first { $0.name == "jpush_reg_id" }?.value, "registration-id-123")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPrivacyGatePreventsSDKInitialization() {
|
||||||
|
let sdk = PushSDKMock(registrationID: "reg-id")
|
||||||
|
let manager = makeManager(sdk: sdk)
|
||||||
|
|
||||||
|
manager.initializeIfPrivacyAccepted()
|
||||||
|
XCTAssertEqual(sdk.initializeCount, 0)
|
||||||
|
|
||||||
|
appStore.session.privacyAgreementAccepted = true
|
||||||
|
manager.initializeIfPrivacyAccepted()
|
||||||
|
manager.initializeIfPrivacyAccepted()
|
||||||
|
XCTAssertEqual(sdk.initializeCount, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEmptyRegistrationIDDoesNotUpload() async {
|
||||||
|
authenticate(userID: "100")
|
||||||
|
let sdk = PushSDKMock(registrationID: nil)
|
||||||
|
let api = PushRegistrationAPIMock()
|
||||||
|
let manager = makeManager(sdk: sdk, api: api)
|
||||||
|
|
||||||
|
manager.handleLoginCompleted()
|
||||||
|
await Task.yield()
|
||||||
|
await Task.yield()
|
||||||
|
|
||||||
|
XCTAssertTrue(api.registrationIDs.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLoggedOutStateDoesNotUpload() async {
|
||||||
|
appStore.session.privacyAgreementAccepted = true
|
||||||
|
let sdk = PushSDKMock(registrationID: "reg-id")
|
||||||
|
let api = PushRegistrationAPIMock()
|
||||||
|
let manager = makeManager(sdk: sdk, api: api)
|
||||||
|
|
||||||
|
manager.initializeIfPrivacyAccepted()
|
||||||
|
await Task.yield()
|
||||||
|
await Task.yield()
|
||||||
|
|
||||||
|
XCTAssertTrue(api.registrationIDs.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLoginRequestsAuthorizationAndUploadsRegistrationIDOnce() async {
|
||||||
|
authenticate(userID: "100")
|
||||||
|
let sdk = PushSDKMock(registrationID: "reg-id")
|
||||||
|
let api = PushRegistrationAPIMock()
|
||||||
|
let manager = makeManager(sdk: sdk, api: api)
|
||||||
|
|
||||||
|
manager.handleLoginCompleted()
|
||||||
|
await waitUntil { api.registrationIDs.count == 1 }
|
||||||
|
manager.retryPendingRegistrationUpload()
|
||||||
|
await Task.yield()
|
||||||
|
|
||||||
|
XCTAssertEqual(sdk.initializeCount, 1)
|
||||||
|
XCTAssertEqual(sdk.authorizationRequestCount, 1)
|
||||||
|
XCTAssertEqual(api.registrationIDs, ["reg-id"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccountSwitchForcesRegistrationIDRebind() async {
|
||||||
|
authenticate(userID: "100")
|
||||||
|
let sdk = PushSDKMock(registrationID: "reg-id")
|
||||||
|
let api = PushRegistrationAPIMock()
|
||||||
|
let manager = makeManager(sdk: sdk, api: api)
|
||||||
|
|
||||||
|
manager.handleLoginCompleted()
|
||||||
|
await waitUntil { api.registrationIDs.count == 1 }
|
||||||
|
appStore.session.userId = "200"
|
||||||
|
manager.handleAccountSwitched()
|
||||||
|
await waitUntil { api.registrationIDs.count == 2 }
|
||||||
|
|
||||||
|
XCTAssertEqual(api.registrationIDs, ["reg-id", "reg-id"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFailedRegistrationUploadRetriesOnNextActivation() async {
|
||||||
|
authenticate(userID: "100")
|
||||||
|
let sdk = PushSDKMock(registrationID: "reg-id")
|
||||||
|
let api = PushRegistrationAPIMock(errors: [PushTestError.expectedFailure, nil])
|
||||||
|
let manager = makeManager(sdk: sdk, api: api)
|
||||||
|
|
||||||
|
manager.handleLoginCompleted()
|
||||||
|
await waitUntil { api.registrationIDs.count == 1 }
|
||||||
|
await waitUntil { api.completedCallCount == 1 }
|
||||||
|
manager.retryPendingRegistrationUpload()
|
||||||
|
await waitUntil { api.registrationIDs.count == 2 }
|
||||||
|
|
||||||
|
XCTAssertEqual(api.registrationIDs, ["reg-id", "reg-id"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNotificationTapRoutesButOrdinaryLifecycleDoesNot() {
|
||||||
|
authenticate(userID: "100")
|
||||||
|
let router = PushRouterMock()
|
||||||
|
let manager = makeManager(router: router)
|
||||||
|
|
||||||
|
manager.initializeIfPrivacyAccepted()
|
||||||
|
XCTAssertTrue(router.destinations.isEmpty)
|
||||||
|
|
||||||
|
manager.handleNotificationTap(
|
||||||
|
payload: PushPayload(userInfo: ["type": 1, "route": "task_management"]),
|
||||||
|
requestIdentifier: "request-1"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(router.destinations, [.paymentRecord])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRemoteNotificationNotifiesUnreadMessageStateChange() async {
|
||||||
|
let manager = makeManager()
|
||||||
|
let unreadChanged = expectation(
|
||||||
|
forNotification: NotificationName.unreadMessageCountDidChange,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
manager.handleRemoteNotification(["route": "message_center"])
|
||||||
|
|
||||||
|
await fulfillment(of: [unreadChanged], timeout: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testApplicationIconBadgeUsesLatestNonnegativeCount() async {
|
||||||
|
let badgeSetter = ApplicationIconBadgeSetterMock()
|
||||||
|
let manager = makeManager(badgeSetter: badgeSetter)
|
||||||
|
|
||||||
|
await manager.updateApplicationIconBadgeCount(12)
|
||||||
|
await manager.updateApplicationIconBadgeCount(-2)
|
||||||
|
|
||||||
|
XCTAssertEqual(badgeSetter.counts, [12, 0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRouteCoordinatorWaitsForLoginAndDeduplicatesColdStartResponse() {
|
||||||
|
let coordinator = PushRouteCoordinator(appStore: appStore)
|
||||||
|
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||||
|
window.rootViewController = UINavigationController(rootViewController: UIViewController())
|
||||||
|
coordinator.attach(window: window)
|
||||||
|
|
||||||
|
coordinator.handle(destination: .paymentDetails, requestIdentifier: "cold-request")
|
||||||
|
authenticate(userID: "100")
|
||||||
|
let mainTab = MainTabBarController()
|
||||||
|
mainTab.loadViewIfNeeded()
|
||||||
|
window.rootViewController = mainTab
|
||||||
|
coordinator.routePendingIfPossible()
|
||||||
|
|
||||||
|
let navigationController = mainTab.selectedViewController as? UINavigationController
|
||||||
|
XCTAssertTrue(navigationController?.topViewController is PaymentCollectionDetailsViewController)
|
||||||
|
|
||||||
|
coordinator.handle(destination: .messageCenter, requestIdentifier: "cold-request")
|
||||||
|
XCTAssertTrue(navigationController?.topViewController is PaymentCollectionDetailsViewController)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func authenticate(userID: String) {
|
||||||
|
appStore.session.privacyAgreementAccepted = true
|
||||||
|
appStore.session.saveToken("token")
|
||||||
|
appStore.session.userId = userID
|
||||||
|
appStore.session.accountType = .storeUser
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeManager(
|
||||||
|
sdk: PushSDKMock? = nil,
|
||||||
|
api: PushRegistrationAPIMock? = nil,
|
||||||
|
router: PushRouterMock? = nil,
|
||||||
|
badgeSetter: ApplicationIconBadgeSetterMock? = nil
|
||||||
|
) -> PushNotificationManager {
|
||||||
|
let resolvedSDK: PushSDKMock
|
||||||
|
if let sdk {
|
||||||
|
resolvedSDK = sdk
|
||||||
|
} else {
|
||||||
|
resolvedSDK = PushSDKMock(registrationID: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolvedAPI: PushRegistrationAPIMock
|
||||||
|
if let api {
|
||||||
|
resolvedAPI = api
|
||||||
|
} else {
|
||||||
|
resolvedAPI = PushRegistrationAPIMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolvedRouter: PushRouterMock
|
||||||
|
if let router {
|
||||||
|
resolvedRouter = router
|
||||||
|
} else {
|
||||||
|
resolvedRouter = PushRouterMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
return PushNotificationManager(
|
||||||
|
sdk: resolvedSDK,
|
||||||
|
api: resolvedAPI,
|
||||||
|
appStore: appStore,
|
||||||
|
defaults: defaults,
|
||||||
|
router: resolvedRouter,
|
||||||
|
applicationIconBadgeSetter: badgeSetter ?? ApplicationIconBadgeSetterMock()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func waitUntil(
|
||||||
|
timeoutIterations: Int = 100,
|
||||||
|
_ condition: () -> Bool
|
||||||
|
) async {
|
||||||
|
for _ in 0..<timeoutIterations {
|
||||||
|
if condition() { return }
|
||||||
|
await Task.yield()
|
||||||
|
}
|
||||||
|
XCTFail("等待异步条件超时")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录 App 桌面图标角标更新的测试替身。
|
||||||
|
@MainActor
|
||||||
|
private final class ApplicationIconBadgeSetterMock: ApplicationIconBadgeSetting {
|
||||||
|
private(set) var counts: [Int] = []
|
||||||
|
|
||||||
|
func setBadgeCount(_ count: Int) async {
|
||||||
|
counts.append(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 可控的极光 SDK 测试替身,记录初始化与通知授权行为。
|
||||||
|
@MainActor
|
||||||
|
private final class PushSDKMock: PushSDKProviding {
|
||||||
|
private let registrationID: String?
|
||||||
|
private(set) var initializeCount = 0
|
||||||
|
private(set) var authorizationRequestCount = 0
|
||||||
|
private(set) var deviceTokens: [Data] = []
|
||||||
|
|
||||||
|
init(registrationID: String?) {
|
||||||
|
self.registrationID = registrationID
|
||||||
|
}
|
||||||
|
|
||||||
|
func initialize(launchOptions: [UIApplication.LaunchOptionsKey: Any]?, isProduction: Bool) {
|
||||||
|
initializeCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestAuthorization(delegate: JPUSHRegisterDelegate) {
|
||||||
|
authorizationRequestCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerDeviceToken(_ deviceToken: Data) {
|
||||||
|
deviceTokens.append(deviceToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchRegistrationID(completion: @escaping (Int32, String?) -> Void) {
|
||||||
|
completion(0, registrationID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 可控制成功或失败结果的 Registration ID 上报接口测试替身。
|
||||||
|
@MainActor
|
||||||
|
private final class PushRegistrationAPIMock: PushRegistrationServing {
|
||||||
|
private var errors: [Error?]
|
||||||
|
private(set) var registrationIDs: [String] = []
|
||||||
|
private(set) var completedCallCount = 0
|
||||||
|
|
||||||
|
init(errors: [Error?] = []) {
|
||||||
|
self.errors = errors
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerJPushID(_ registrationID: String) async throws {
|
||||||
|
registrationIDs.append(registrationID)
|
||||||
|
let error = errors.isEmpty ? nil : errors.removeFirst()
|
||||||
|
completedCallCount += 1
|
||||||
|
if let error { throw error }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记录通知点击路由行为的测试替身。
|
||||||
|
@MainActor
|
||||||
|
private final class PushRouterMock: PushRouting {
|
||||||
|
private(set) var destinations: [PushDestination] = []
|
||||||
|
private(set) var resetCount = 0
|
||||||
|
|
||||||
|
func attach(window: UIWindow) {}
|
||||||
|
|
||||||
|
func handle(destination: PushDestination, requestIdentifier: String) {
|
||||||
|
destinations.append(destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
func routePendingIfPossible() {}
|
||||||
|
|
||||||
|
func resetPendingRoute() {
|
||||||
|
resetCount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 推送单元测试使用的可预期失败错误。
|
||||||
|
private enum PushTestError: Error {
|
||||||
|
case expectedFailure
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user