- 苹果支付、分享

This commit is contained in:
linshujie 2026-07-06 15:23:28 +08:00
parent 4269af8611
commit 6163dd1ca8
46 changed files with 1376 additions and 271 deletions

View File

@ -225,6 +225,8 @@
30C4C01D2FDBF557009215C1 /* RemoveMemberViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C4C01C2FDBF557009215C1 /* RemoveMemberViewModel.swift */; };
30C4C0202FDC0EC5009215C1 /* GroupInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C4C01F2FDC0EC5009215C1 /* GroupInfoView.swift */; };
30C4C0222FDC0ED3009215C1 /* GroupInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C4C0212FDC0ED3009215C1 /* GroupInfoVC.swift */; };
30C6666C2FFB7C4000E62B25 /* IAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C6666B2FFB7C4000E62B25 /* IAPManager.swift */; };
30C6679E2FFB7FEF00E62B25 /* SharePopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C6679C2FFB7FEF00E62B25 /* SharePopView.swift */; };
30CCDE4E2FE26CEA00F5214A /* PayResultPopVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CCDE4D2FE26CEA00F5214A /* PayResultPopVC.swift */; };
30CCDE512FE2785D00F5214A /* SignInVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CCDE502FE2785D00F5214A /* SignInVC.swift */; };
30CCDE532FE2786600F5214A /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CCDE522FE2786600F5214A /* SignInView.swift */; };
@ -524,6 +526,8 @@
30C4C01C2FDBF557009215C1 /* RemoveMemberViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveMemberViewModel.swift; sourceTree = "<group>"; };
30C4C01F2FDC0EC5009215C1 /* GroupInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupInfoView.swift; sourceTree = "<group>"; };
30C4C0212FDC0ED3009215C1 /* GroupInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupInfoVC.swift; sourceTree = "<group>"; };
30C6666B2FFB7C4000E62B25 /* IAPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPManager.swift; sourceTree = "<group>"; };
30C6679C2FFB7FEF00E62B25 /* SharePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePopView.swift; sourceTree = "<group>"; };
30CCDE4D2FE26CEA00F5214A /* PayResultPopVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayResultPopVC.swift; sourceTree = "<group>"; };
30CCDE502FE2785D00F5214A /* SignInVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInVC.swift; sourceTree = "<group>"; };
30CCDE522FE2786600F5214A /* SignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInView.swift; sourceTree = "<group>"; };
@ -910,6 +914,7 @@
305A752A2FCA8C7000227D26 /* Theme */,
305A752C2FCA8C7000227D26 /* URL */,
30D87CD52FDF9F1900E958FD /* MQTT */,
30C6666A2FFB7C3000E62B25 /* IAPManager */,
);
path = Manager;
sourceTree = "<group>";
@ -1072,6 +1077,7 @@
30DC18592FD11E7A0041DCD1 /* Web */,
30ACD54A2FF6332200174861 /* PopupWindow */,
30EFF3AD2FD7FF1400EB35D4 /* TextInput */,
30C6679D2FFB7FEF00E62B25 /* Share */,
);
path = Section;
sourceTree = "<group>";
@ -1436,6 +1442,22 @@
path = GroupInfo;
sourceTree = "<group>";
};
30C6666A2FFB7C3000E62B25 /* IAPManager */ = {
isa = PBXGroup;
children = (
30C6666B2FFB7C4000E62B25 /* IAPManager.swift */,
);
path = IAPManager;
sourceTree = "<group>";
};
30C6679D2FFB7FEF00E62B25 /* Share */ = {
isa = PBXGroup;
children = (
30C6679C2FFB7FEF00E62B25 /* SharePopView.swift */,
);
path = Share;
sourceTree = "<group>";
};
30CCDE4F2FE2782700F5214A /* SignIn */ = {
isa = PBXGroup;
children = (
@ -1840,6 +1862,7 @@
305A769C2FCA8C7000227D26 /* PopupViewController.swift in Sources */,
305A769D2FCA8C7000227D26 /* PopupViewController+Extension.swift in Sources */,
30EFF3C42FDA431D00EB35D4 /* ChangePhoneVC.swift in Sources */,
30C6666C2FFB7C4000E62B25 /* IAPManager.swift in Sources */,
305A769E2FCA8C7000227D26 /* TextArrowCell.swift in Sources */,
305A769F2FCA8C7000227D26 /* TextContentArrowCell.swift in Sources */,
30D87D042FE1336300E958FD /* NavigationVC.swift in Sources */,
@ -1925,6 +1948,7 @@
305A76CA2FCA8C7000227D26 /* AppSettings.swift in Sources */,
305A76CB2FCA8C7000227D26 /* Authorize.swift in Sources */,
30CCDE552FE2903100F5214A /* SignInModel.swift in Sources */,
30C6679E2FFB7FEF00E62B25 /* SharePopView.swift in Sources */,
305A76CC2FCA8C7000227D26 /* FileTools.swift in Sources */,
305A76CD2FCA8C7000227D26 /* Permission.swift in Sources */,
30D74AB02FEA13ED0050EB2C /* ScheduleVC.swift in Sources */,

View File

@ -23,6 +23,9 @@ enum OrderAPI {
/// - source: center
/// - extra:
case orderPayParams(goodsId: String, payType: String, source: String, extra: [String:Any]=[:])
/// Apple
case appleNotify(receipt: String, productId: String, transactionId: String)
}
extension OrderAPI: MultiTargetProtocol {
@ -33,6 +36,8 @@ extension OrderAPI: MultiTargetProtocol {
return "api/order/goods"
case .orderPayParams:
return "api/order"
case .appleNotify:
return "api/order/notify/apple"
}
}
@ -60,6 +65,13 @@ extension OrderAPI: MultiTargetProtocol {
params["pay_source"] = "app"
params["extra"] = extra
return .requestParameters(parameters: params, encoding: JSONEncoding())
case let .appleNotify(receipt, productId, transactionId):
var params = Parameters()
params["receipt_data"] = receipt
params["product_id"] = productId
params["transaction_id"] = transactionId
return .requestParameters(parameters: params, encoding: JSONEncoding())
}
}
}

View File

@ -29,6 +29,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
IAPManager.shared.startObserving()
IQKeyboardManager.shared.isEnabled = true
IQKeyboardManager.shared.enableAutoToolbar = false
IQKeyboardManager.shared.resignOnTouchOutside = true

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_2432@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_2432@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "normal_on_bg@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "normal_on_bg@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "recommend_bg@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "recommend_bg@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_2429@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_2429@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "recommend_on_bg@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "recommend_on_bg@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_2428@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_2428@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "friend@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "friend@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "moments@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "moments@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "url@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "url@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "apple@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "apple@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -116,6 +116,8 @@ struct PopupWindowTimeModel: Mappable {
var pay_pop_time: Int = 0
/// vip
var min_discount: Double = 0
/// 1 2
var pay_pop_type: Int = 0
init?(map: Map) {
@ -127,5 +129,6 @@ struct PopupWindowTimeModel: Mappable {
signup_tip <- map["signup_tip"]
pay_pop_time <- map["pay_pop_time"]
min_discount <- map["min_discount"]
pay_pop_type <- map["pay_pop_type"]
}
}

View File

@ -0,0 +1,81 @@
//
// IAPManager.swift
// QuickLocation
//
// Created by on 2026/7/3.
//
import StoreKit
import RxSwift
class IAPManager: NSObject {
static let shared = IAPManager()
private let disposeBag = DisposeBag()
private var currentGoodsId: String = ""
private var purchaseCompletion: ((Bool, String?) -> Void)?
func startObserving() {
SKPaymentQueue.default().add(self)
}
func purchase(goodsId: String, completion: @escaping (Bool, String?) -> Void) {
currentGoodsId = goodsId
purchaseCompletion = completion
let request = SKProductsRequest(productIdentifiers: [goodsId])
request.delegate = self
request.start()
}
}
// MARK: - SKProductsRequestDelegate
extension IAPManager: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
guard let product = response.products.first else {
purchaseCompletion?(false, "商品不可用")
return
}
SKPaymentQueue.default().add(SKPayment(product: product))
}
}
// MARK: - SKPaymentTransactionObserver
extension IAPManager: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for tx in transactions {
switch tx.transactionState {
case .purchased:
verifyReceipt(transaction: tx)
case .failed:
queue.finishTransaction(tx)
let msg = tx.error?.localizedDescription ?? "支付失败"
purchaseCompletion?(false, msg)
default:
break
}
}
}
private func verifyReceipt(transaction: SKPaymentTransaction) {
guard let receiptURL = Bundle.main.appStoreReceiptURL,
let receiptData = try? Data(contentsOf: receiptURL) else {
SKPaymentQueue.default().finishTransaction(transaction)
purchaseCompletion?(false, "获取凭证失败")
return
}
let receipt = receiptData.base64EncodedString()
let txId = transaction.transactionIdentifier ?? ""
OrderService.appleNotify(
receipt: receipt,
productId: currentGoodsId,
transactionId: txId
).subscribe(onNext: { [weak self] _ in
SKPaymentQueue.default().finishTransaction(transaction)
self?.purchaseCompletion?(true, nil)
}, onError: { [weak self] error in
SKPaymentQueue.default().finishTransaction(transaction)
self?.purchaseCompletion?(false, error.localizedDescription)
}).disposed(by: disposeBag)
}
}

View File

@ -8,6 +8,7 @@
import UIKit
import ObjectMapper
import RxSwift
import RxCocoa
class InviteJoinVC: BaseViewController {
@ -25,8 +26,13 @@ class InviteJoinVC: BaseViewController {
// Do any additional setup after loading the view.
guard let model = groupInModel else { return }
//
rootView.shareAppBtn.rx.tap.subscribe(onNext: { _ in
let url = "https://smartdrive.zuom8.cn/jisu/share/user/"
SharePopView.show(shareURL: url)
}).disposed(by: disposeBag)
guard let model = groupInModel else { return }
rootView.groupIcon.image = model.groupIcon
rootView.groupNameLab.text = model.name
requestOperateGroup(key: model.group_key)

View File

@ -246,6 +246,13 @@ class HomeViewController: BaseViewController {
guard let self = self else { return }
self.requestSendEmote(emoteIdx: emoteIdx)
}
//
rootView.interactionView.onShare = { [weak self] in
guard let self = self, let member = self.rootView.interactionView.currentMember else { return }
let url = "https://smartdrive.zuom8.cn/jisu/share/user/\(member.id)"
SharePopView.show(shareURL: url)
}
}
private func bindViewModel() {

View File

@ -68,6 +68,12 @@ class InteractionView: UIView {
})
.disposed(by: disposeBag)
shareBtn.rx.tap
.subscribe(onNext: { [weak self] _ in
self?.onShare?()
})
.disposed(by: disposeBag)
// collectionView
emojiRelay
.map { [SectionModel(model: "", items: $0)] }
@ -474,6 +480,7 @@ class InteractionView: UIView {
var onNavigate: (() -> Void)?
var onSendEmote: ((Int) -> Void)?
var onShare: (() -> Void)?
var currentMember: CircleMember?
lazy var emojiPageControl: UIPageControl = {

View File

@ -10,10 +10,37 @@ import RxSwift
import RxCocoa
import RxDataSources
extension PromotionalActivitiesItem: Equatable {
static func == (lhs: Self, rhs: Self) -> Bool {
switch (lhs, rhs) {
case (.recommend(let a), .recommend(let b)): return a == b
case (.normal(let a), .normal(let b)): return a == b
default: return false
}
}
}
extension PromotionalActivitiesItem: IdentifiableType {
typealias Identity = String
var identity: String {
switch self {
case .recommend(let model): return model.goods_id
case .normal(let model): return model.goods_id
}
}
}
class PromotionalActivitiesVC: BaseViewController {
fileprivate var rootView: PromotionalActivitiesView!
var onDismiss: (() -> Void)?
private var selectedGoodsId: String = ""
private let itemsRelay = BehaviorRelay<[PromotionalActivitiesItem]>(value: [])
var items: [PromotionalActivitiesItem] {
get { itemsRelay.value }
set { itemsRelay.accept(newValue) }
}
override func loadView() {
rootView = PromotionalActivitiesView(frame: UIScreen.main.bounds)
@ -23,8 +50,50 @@ class PromotionalActivitiesVC: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
setupTableView()
requestRechargeInfo()
rootView.confirmBtn.rx.tap.subscribe(onNext: { _ in
self.requestOrderPayParams()
}).disposed(by: disposeBag)
}
private func setupTableView() {
typealias Section = SectionModel<String, PromotionalActivitiesItem>
let dataSource = RxTableViewSectionedReloadDataSource<Section> { _, tableView, indexPath, item in
let isSelected = item.identity == self.selectedGoodsId
switch item {
case .recommend(let model):
let cell: PromotionalRecommendCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(model: model, isSelected: isSelected)
return cell
case .normal(let model):
let cell: PromotionalNormalCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(model: model, isSelected: isSelected)
return cell
}
}
itemsRelay
.map { [SectionModel(model: "", items: $0)] }
.bind(to: rootView.tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
itemsRelay
.subscribe(onNext: { [weak self] items in
self?.rootView.tableView.layoutChain.height(CGFloat(items.count) * 106 + 5)
self?.rootView.scrollContentView.layoutIfNeeded()
})
.disposed(by: disposeBag)
rootView.tableView.rx.itemSelected
.subscribe(onNext: { [weak self] indexPath in
guard let self = self else { return }
let item = self.itemsRelay.value[indexPath.row]
self.selectedGoodsId = item.identity
self.rootView.tableView.reloadData()
})
.disposed(by: disposeBag)
}
override func viewDidDisappear(_ animated: Bool) {
@ -39,7 +108,103 @@ class PromotionalActivitiesVC: BaseViewController {
DLToast.showLoading()
OrderService.rechargeInfo(type: "member").subscribe(onNext: { [weak self] response in
guard let self = self else { return }
self.rootView.startCountdown(endTime: 1769875199)
if let checkedId = response.list.first(where: { $0.checked })?.goods_id {
self.selectedGoodsId = checkedId
}
let items = response.list.map { model in
model.goods_id == "20042"
? PromotionalActivitiesItem.recommend(model: model)
: PromotionalActivitiesItem.normal(model: model)
}
self.items = items
self.rootView.startCountdown()
}).disposed(by: disposeBag)
}
private func requestOrderPayParams() {
guard let systemConfig = AppContextManager.shared.systemConfig, let timeConfig = systemConfig.timeConfig else {
return
}
if timeConfig.pay_pop_type == 3 {
IAPManager.shared.purchase(goodsId: selectedGoodsId) { [weak self] success, msg in
guard let self = self else { return }
if success {
self.showPayResultPop(showCloseBtn: false,
status: true,
title: "支付成功",
message: "恭喜您!成功开通!",
confirmText: "确定", confirmBlock: {
AppRouter.shared.popOrDismiss()
}, cancelBlock: { })
} else {
self.showPayResultPop(status: false,
title: "支付失败",
message: msg ?? "很抱歉,请您重新支付!",
confirmText: "重新支付", confirmBlock: { },
cancelText: "取消", cancelBlock: {
AppRouter.shared.popOrDismiss()
})
}
}
return
}
let payType = timeConfig.pay_pop_type == 1 ? "alipay" : "weixin"
dl.showLoading()
OrderService.orderPayParams(goodsId: selectedGoodsId,
payType: payType).subscribe(onNext: { response in
self.dl.dismiss()
guard let data = response.data else { return }
if payType == "weixin" { //
// WXApi.registerApp(model.appId, universalLink: AppSettings.kAppsUniversalLink)
//
// let request: PayReq = PayReq()
// request.partnerId = model.partnerId
// request.prepayId = model.prepayId
// request.package = model.packageValue
// request.nonceStr = model.nonceStr
// request.timeStamp = UInt32(model.timeStamp) ?? 0
// request.sign = model.sign
// WXApi.send(request)
}
else if payType == "alipay" { //
if let payParam = data["payParam"] as? String, let appId = data["payParam"] as? String {
AlipaySDK.defaultService().payOrder(payParam, fromScheme: "Alipay\(appId)") { resultDic in
print("支付宝callback -> \(resultDic)")
guard let result = resultDic,
let resultStatus = result["resultStatus"] as? String else { return }
/**
9000
8000
4000
5000
6001
6002
6004
*/
if resultStatus == "9000" {
self.showPayResultPop(showCloseBtn: false,
status: true,
title: "支付成功",
message: "恭喜您!成功开通\("")",
confirmText: "确定", confirmBlock: {
AppRouter.shared.popOrDismiss()
}, cancelBlock: { })
}
else {
self.showPayResultPop(status: false,
title: "支付失败",
message: "很抱歉,请您重新支付!",
confirmText: "重新支付", confirmBlock: { }, cancelText: "取消", cancelBlock: {
AppRouter.shared.popOrDismiss()
})
}
}
}
}
}, onError: { _ in }).disposed(by: disposeBag)
}
}

View File

@ -9,6 +9,11 @@ import UIKit
import RxSwift
import RxCocoa
enum PromotionalActivitiesItem {
case recommend(model: VipExpenseModel)
case normal(model: VipExpenseModel)
}
class PromotionalActivitiesView: UIView {
var disposeBag = DisposeBag()
@ -17,7 +22,6 @@ class PromotionalActivitiesView: UIView {
backBtn.rx.tap.subscribe(onNext: { _ in
VipWaivePopView.show()
}).disposed(by: disposeBag)
}
private func setupUI() {
@ -121,13 +125,18 @@ class PromotionalActivitiesView: UIView {
.leftToRightOfView(iconView, offset: 4)
.centerY(iconView)
scrollContentView.addSubview(tableView)
tableView.layoutChain
.topToBottomOfView(titleLab, offset: 0)
.edgesHorzontal()
let agreementView = UIView()
infoView.addSubview(agreementView)
agreementView.layoutChain
.topToBottomOfView(titleLab, offset: 18)
.topToBottomOfView(tableView, offset: 15)
.left(15)
.right(15)
.height(30)
.height(16)
agreementView.addSubview(agreementCheckBtn)
agreementCheckBtn.layoutChain
@ -138,7 +147,7 @@ class PromotionalActivitiesView: UIView {
agreementView.addSubview(agreementTextView)
agreementTextView.layoutChain
.leftToRightOfView(agreementCheckBtn, offset: 6)
.leftToRightOfView(agreementCheckBtn, offset: 3)
.right(0)
.centerY()
.bottom(0)
@ -161,12 +170,24 @@ class PromotionalActivitiesView: UIView {
return view
}()
lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.estimatedRowHeight = 106
tableView.isScrollEnabled = false
tableView.showsVerticalScrollIndicator = false
tableView.register(PromotionalNormalCell.self)
tableView.register(PromotionalRecommendCell.self)
return tableView
}()
lazy var bottomView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.addSubview(countdownLabel)
countdownLabel.layoutChain
view.addSubview(countdownContainer)
countdownContainer.layoutChain
.top(11)
.centerX()
@ -186,6 +207,7 @@ class PromotionalActivitiesView: UIView {
btn.rx.tap.subscribe(onNext: { [weak btn] in
btn?.isSelected.toggle()
}).disposed(by: disposeBag)
btn.isSelected = true
return btn
}()
@ -212,9 +234,59 @@ class PromotionalActivitiesView: UIView {
return tv
}()
lazy var countdownLabel: UILabel = {
lazy var countdownContainer: UIStackView = {
let stack = UIStackView(arrangedSubviews: [countdownPrefixLabel, countdownMinLabel, countdownColonLabel, countdownSecLabel, countdownSuffixLabel])
stack.axis = .horizontal
stack.alignment = .center
stack.spacing = 0
return stack
}()
lazy var countdownPrefixLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12)
label.textColor = UIColor(hexStr: "#FD501B")
label.text = "优惠活动倒计时 "
return label
}()
lazy var countdownMinLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12)
label.textColor = .white
label.backgroundColor = UIColor(hexStr: "#FD501B")
label.layer.cornerRadius = 2
label.clipsToBounds = true
label.textAlignment = .center
label.text = "15"
return label
}()
lazy var countdownColonLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12)
label.textColor = UIColor(hexStr: "#FD501B")
label.text = ":"
return label
}()
lazy var countdownSecLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12)
label.textColor = .white
label.backgroundColor = UIColor(hexStr: "#FD501B")
label.layer.cornerRadius = 2
label.clipsToBounds = true
label.textAlignment = .center
label.text = "00"
return label
}()
lazy var countdownSuffixLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12)
label.textColor = UIColor(hexStr: "#FD501B")
label.text = " 限时特惠"
return label
}()
@ -228,11 +300,11 @@ class PromotionalActivitiesView: UIView {
return btn
}()
private var countdownEndTime: TimeInterval = 0
private var countdownRemaining: Int = 15 * 60
private var countdownTimer: Timer?
func startCountdown(endTime: TimeInterval) {
countdownEndTime = endTime
func startCountdown() {
countdownRemaining = 15 * 60
updateCountdown()
countdownTimer?.invalidate()
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
@ -246,38 +318,17 @@ class PromotionalActivitiesView: UIView {
}
private func updateCountdown() {
let remaining = max(0, countdownEndTime - Date().timeIntervalSince1970)
let totalSeconds = Int(remaining)
let days = totalSeconds / 86400
let hours = (totalSeconds % 86400) / 3600
let minutes = (totalSeconds % 3600) / 60
let seconds = totalSeconds % 60
let attr = NSMutableAttributedString()
let normalStyle: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor(hexStr: "#FD501B"),
.font: UIFont.systemFont(ofSize: 12)
]
let timeStyle: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.white,
.font: UIFont.systemFont(ofSize: 12),
.backgroundColor: UIColor(hexStr: "#FD501B")
]
attr.append(NSAttributedString(string: "优惠活动倒计时 ", attributes: normalStyle))
let timeStr: String
if days > 0 {
timeStr = String(format: "%d天%02d:%02d:%02d", days, hours, minutes, seconds)
} else {
timeStr = String(format: "%02d:%02d:%02d", hours, minutes, seconds)
if countdownRemaining > 0 {
countdownRemaining -= 1
}
attr.append(NSAttributedString(string: timeStr, attributes: timeStyle))
attr.append(NSAttributedString(string: " 限时特惠", attributes: normalStyle))
countdownLabel.attributedText = attr
else {
AppRouter.shared.popOrDismiss()
return
}
let min = countdownRemaining / 60
let sec = countdownRemaining % 60
countdownMinLabel.text = String(format: "%02d", min)
countdownSecLabel.text = String(format: "%02d", sec)
}
override func layoutSubviews() {
@ -319,3 +370,287 @@ extension PromotionalActivitiesView: UITextViewDelegate {
return false
}
}
// MARK: - PromotionalNormalCell
class PromotionalNormalCell: UITableViewCell {
func configure(model: VipExpenseModel, isSelected: Bool) {
priceLab.text = model.price
originPriceLab.text = "原价" + model.origin_price
originPriceLab.setupStrikethroughStyle()
goodsNameLab.text = model.goods_name
tipsLab.text = model.tips2
bgView.image = UIImage(named: isSelected ? "PromotionalActivities/normal_on_bg" : "PromotionalActivities/normal_bg")
priceLab.textColor = isSelected ? UIColor(hexStr: "#FF383C") : ThemeManager.shared.color.titleAuxColor
unitLab.textColor = isSelected ? UIColor(hexStr: "#FF383C") : ThemeManager.shared.color.titleAuxColor
}
override init(style: CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
setupSubviews()
}
private func setupSubviews() {
contentView.addSubview(bgView)
bgView.addSubview(unitLab)
bgView.addSubview(priceLab)
bgView.addSubview(originPriceLab)
bgView.addSubview(goodsNameLab)
bgView.addSubview(tipsLab)
bgView.layoutChain
.top(22)
.edgesHorzontal(15)
.bottom()
priceLab.layoutChain
.leftToRightOfView(unitLab, offset: 0)
.centerY()
unitLab.layoutChain
.left(32)
.centerY(bgView, offset: 9)
originPriceLab.layoutChain
.topToBottomOfView(priceLab, offset: -5)
.centerX(priceLab)
goodsNameLab.layoutChain
.leftToRightOfView(priceLab, offset: 27)
.bottomToCenterYOfView(bgView)
tipsLab.layoutChain
.leftToView(goodsNameLab)
.topToCenterYOfView(bgView, offset: 3)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
lazy var bgView: UIImageView = {
let view = UIImageView()
view.backgroundColor = .clear
view.contentMode = .scaleAspectFill
return view
}()
lazy var priceLab: UILabel = {
let label = UILabel()
label.textColor = UIColor(hexStr: "#FF383C")
label.font = .systemFont(ofSize: 40, weight: .bold)
return label
}()
lazy var unitLab: UILabel = {
let label = UILabel()
label.text = ""
label.textColor = UIColor(hexStr: "#FF383C")
label.font = .systemFont(ofSize: 12, weight: .regular)
return label
}()
lazy var originPriceLab: UILabel = {
let label = UILabel()
label.textColor = UIColor(hexStr: "#999999")
label.font = .systemFont(ofSize: 10, weight: .regular)
return label
}()
lazy var goodsNameLab: UILabel = {
let label = UILabel()
label.textColor = ThemeManager.shared.color.titleAuxColor
label.font = .systemFont(ofSize: 18, weight: .semibold)
return label
}()
lazy var tipsLab: UILabel = {
let label = UILabel()
label.textColor = ThemeManager.shared.color.contentColor
label.font = .systemFont(ofSize: 14, weight: .regular)
return label
}()
}
// MARK: - PromotionalRecommendCell
class PromotionalRecommendCell: UITableViewCell {
func configure(model: VipExpenseModel, isSelected: Bool) {
tipsTitleLab.text = model.tips
priceLab.text = model.price
originPriceLab.text = "原价" + model.origin_price
originPriceLab.setupStrikethroughStyle()
goodsNameLab.text = model.goods_name
tipsLab.text = model.tips2
bgView.image = UIImage(named: isSelected ? "PromotionalActivities/recommend_on_bg" : "PromotionalActivities/recommend_bg")
priceLab.textColor = isSelected ? UIColor(hexStr: "#FF383C") : ThemeManager.shared.color.titleAuxColor
unitLab.textColor = isSelected ? UIColor(hexStr: "#FF383C") : ThemeManager.shared.color.titleAuxColor
}
override init(style: CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
setupSubviews()
}
private func setupSubviews() {
contentView.addSubview(bgView)
bgView.addSubview(unitLab)
bgView.addSubview(priceLab)
bgView.addSubview(originPriceLab)
bgView.addSubview(goodsNameLab)
bgView.addSubview(tipsLab)
bgView.addSubview(recommendLab)
bgView.addSubview(recommendIcon)
bgView.addSubview(tipsTitleView)
tipsTitleView.addSubview(tipsTitleLab)
bgView.layoutChain
.top(22)
.edgesHorzontal(15)
.bottom()
tipsTitleView.layoutChain
.topToView(bgView, offset: -8)
.leftToView(bgView, offset: -6)
.width(94)
.height(35)
tipsTitleLab.layoutChain
.centerX()
.top(5)
priceLab.layoutChain
.leftToRightOfView(unitLab, offset: 0)
.centerY()
unitLab.layoutChain
.left(32)
.centerY(bgView, offset: 9)
originPriceLab.layoutChain
.topToBottomOfView(priceLab, offset: -5)
.centerX(priceLab)
goodsNameLab.layoutChain
.leftToRightOfView(priceLab, offset: 27)
.bottomToCenterYOfView(bgView, offset: -5)
tipsLab.layoutChain
.leftToView(goodsNameLab)
.topToCenterYOfView(bgView, offset: 8)
recommendLab.layoutChain
.top(8)
.right(24)
recommendIcon.layoutChain
.rightToLeftOfView(recommendLab, offset: -3)
.centerY(recommendLab)
.width(13)
.height(11)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
lazy var bgView: UIImageView = {
let view = UIImageView()
view.backgroundColor = .clear
view.contentMode = .scaleAspectFill
return view
}()
lazy var priceLab: UILabel = {
let label = UILabel()
label.textColor = UIColor(hexStr: "#FF383C")
label.font = .systemFont(ofSize: 40, weight: .bold)
return label
}()
lazy var unitLab: UILabel = {
let label = UILabel()
label.text = ""
label.textColor = UIColor(hexStr: "#FF383C")
label.font = .systemFont(ofSize: 12, weight: .regular)
return label
}()
lazy var originPriceLab: UILabel = {
let label = UILabel()
label.textColor = UIColor(hexStr: "#999999")
label.font = .systemFont(ofSize: 10, weight: .regular)
return label
}()
lazy var goodsNameLab: UILabel = {
let label = UILabel()
label.textColor = ThemeManager.shared.color.titleAuxColor
label.font = .systemFont(ofSize: 18, weight: .semibold)
return label
}()
lazy var tipsTitleView: UIImageView = {
let view = UIImageView()
view.backgroundColor = .clear
view.image = UIImage(named: "PromotionalActivities/tips_bg")
return view
}()
lazy var tipsTitleLab: UILabel = {
let label = UILabel()
label.textColor = .white
label.font = .systemFont(ofSize: 12, weight: .semibold)
return label
}()
lazy var tipsLab: UILabel = {
let label = UILabel()
label.textColor = ThemeManager.shared.color.contentColor
label.font = .systemFont(ofSize: 14, weight: .regular)
return label
}()
lazy var recommendIcon: UIImageView = {
let view = UIImageView()
view.contentMode = .scaleAspectFill
view.image = UIImage(named: "PromotionalActivities/recommend_icon")
return view
}()
lazy var recommendLab: UILabel = {
let label = UILabel()
label.text = "80%的用户选择"
label.textColor = .white
label.font = .systemFont(ofSize: 12, weight: .medium)
return label
}()
}

View File

@ -0,0 +1,199 @@
//
// SharePopView.swift
// QuickLocation
//
// Created by on 2026/7/3.
//
import UIKit
class SharePopView: UIView {
private static let shared = SharePopView(frame: CGRect(origin: .zero, size: kScreenSize))
private var shareURL: String = ""
private let contentHeight: CGFloat = 191 + kSafeBottomMargin
static func show(shareURL: String) {
guard let superView = kKeyWindow else { return }
if SharePopView.shared.superview != nil {
SharePopView.shared.dismiss(animated: false)
}
SharePopView.shared.shareURL = shareURL
SharePopView.shared.bgView.alpha = 0
superView.addSubview(SharePopView.shared)
superView.bringSubviewToFront(SharePopView.shared)
SharePopView.shared.contentView.transform = CGAffineTransform(translationX: 0, y: SharePopView.shared.contentHeight)
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut) {
SharePopView.shared.bgView.alpha = 1
SharePopView.shared.contentView.transform = .identity
}
}
private func dismiss(animated: Bool = true) {
if animated {
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseIn) {
self.bgView.alpha = 0
self.contentView.transform = CGAffineTransform(translationX: 0, y: self.contentHeight)
} completion: { _ in
self.removeFromSuperview()
}
} else {
removeFromSuperview()
}
}
@objc private func handleBgTap() {
dismiss()
}
@objc private func handleWeChatTap() {
shareToWeChat(scene: 0)
}
@objc private func handleMomentsTap() {
shareToWeChat(scene: 1)
}
@objc private func handleLinkTap() {
UIPasteboard.general.string = shareURL
DLToast.showSuccess(text: "链接已复制")
dismiss()
}
private func shareToWeChat(scene: Int32) {
let message = WXMediaMessage()
message.title = kAppName
let webpage = WXWebpageObject()
webpage.webpageUrl = shareURL
message.mediaObject = webpage
let req = SendMessageToWXReq()
req.bText = false
req.message = message
req.scene = scene
WXApi.send(req)
dismiss()
}
// MARK: - UI
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
addSubview(bgView)
bgView.addSubview(contentView)
contentView.addSubview(titleLabel)
contentView.addSubview(shareStack)
contentView.addSubview(closeBtn)
bgView.layoutChain.edges()
contentView.layoutChain
.edges(excludingEdge: .top)
.height(contentHeight)
titleLabel.layoutChain
.top(20)
.centerX()
closeBtn.layoutChain
.centerY(titleLabel)
.right(20)
.width(20)
.height(20)
shareStack.layoutChain
.topToBottomOfView(titleLabel, offset: 30)
.centerX()
let tap = UITapGestureRecognizer(target: self, action: #selector(handleBgTap))
bgView.addGestureRecognizer(tap)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Lazy Views
private lazy var bgView: UIView = {
let view = UIView()
view.backgroundColor = .black.withAlphaComponent(0.5)
return view
}()
private lazy var contentView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 26
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.clipsToBounds = true
return view
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = "分享"
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = UIColor(hexStr: "#333333")
return label
}()
private lazy var shareStack: UIStackView = {
let stack = UIStackView(arrangedSubviews: [
makeShareItem(icon: "Share/friend", title: "微信", action: #selector(handleWeChatTap)),
makeShareItem(icon: "Share/moments", title: "朋友圈", action: #selector(handleMomentsTap)),
makeShareItem(icon: "Share/url", title: "链接", action: #selector(handleLinkTap))
])
stack.axis = .horizontal
stack.spacing = 50
stack.alignment = .center
return stack
}()
private lazy var closeBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Group/close"), for: .normal)
btn.addTarget(self, action: #selector(handleBgTap), for: .touchUpInside)
btn.extendEdgeInsets = UIEdgeInsets(top: 15, left: 20, bottom: 20, right: 20)
return btn
}()
private func makeShareItem(icon: String, title: String, action: Selector) -> UIView {
let container = UIView()
let imgView = UIImageView(image: UIImage(named: icon))
imgView.contentMode = .scaleAspectFit
container.addSubview(imgView)
imgView.layoutChain
.top(0)
.centerX()
.width(50)
.height(50)
let label = UILabel()
label.text = title
label.font = .systemFont(ofSize: 12)
label.textColor = UIColor(hexStr: "#666666")
label.textAlignment = .center
container.addSubview(label)
label.layoutChain
.topToBottomOfView(imgView, offset: 8)
.centerX()
.bottom(0)
container.layoutChain.width(60).height(78)
let btn = UIButton(type: .custom)
container.addSubview(btn)
btn.layoutChain.edges()
btn.addTarget(self, action: action, for: .touchUpInside)
return container
}
}

View File

@ -99,8 +99,34 @@ class VipRechargeVC: BaseViewController {
// MARK: - API
private func requestOrderPayParams() {
DLToast.showLoading()
let types = viewModel.payType.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) }
let payType = types[rootView.selectedPayTypeTag]
if payType == "apple" {
IAPManager.shared.purchase(goodsId: viewModel.goodsId) { [weak self] success, msg in
guard let self = self else { return }
if success {
self.showPayResultPop(showCloseBtn: false,
status: true,
title: "支付成功",
message: "恭喜您!成功开通\(self.viewModel.goodsName)",
confirmText: "确定", confirmBlock: {
AppRouter.shared.popOrDismiss()
}, cancelBlock: { })
} else {
self.showPayResultPop(status: false,
title: "支付失败",
message: msg ?? "很抱歉,请您重新支付!",
confirmText: "重新支付", confirmBlock: { },
cancelText: "取消", cancelBlock: {
AppRouter.shared.popOrDismiss()
})
}
}
return
}
DLToast.showLoading()
OrderService.orderPayParams(goodsId: viewModel.goodsId,
payType: types[rootView.selectedPayTypeTag]).subscribe(onNext: { response in
guard let data = response.data else { return }

View File

@ -562,6 +562,7 @@ class VipRechargeView: UIView {
let payTypeMap: [(String, String)] = types.compactMap {
if $0 == "weixin" { return ("wechat", "微信支付") }
if $0 == "alipay" { return ("alipay", "支付宝支付") }
if $0 == "apple" { return ("apple", "苹果支付") }
return nil
}
guard !payTypeMap.isEmpty else { return }

View File

@ -33,4 +33,12 @@ struct OrderService {
.map(ResponseModel.self)
.asObservable()
}
/// Apple
static func appleNotify(receipt: String, productId: String, transactionId: String) -> Observable<Void> {
let api = OrderAPI.appleNotify(receipt: receipt, productId: productId, transactionId: transactionId).multiTarget
return APIProvider.request(token: api)
.map { _ in () }
.asObservable()
}
}