From 318a4d9b7ef569fdcfba3af7f7d68bb436217e68 Mon Sep 17 00:00:00 2001 From: zeng Date: Wed, 30 Jul 2025 08:50:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=86=85=E8=B4=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BeeReel.xcodeproj/project.pbxproj | 40 ++++ BeeReel/Base/Define/BRUserDefaultsKey.swift | 2 + BeeReel/Base/Network/API/BRStoreAPI.swift | 64 ++++++ .../BRCoinOrderRecordViewController.swift | 37 +++- .../Controller/BRStoreViewController.swift | 21 +- .../Store/Model/BRRechargeRecordModel.swift | 15 ++ .../Store/View/BRCoinOrderRecordCell.swift | 6 + .../Class/Store/View/BRStoreCoinView.swift | 11 +- BeeReel/Class/Store/View/BRStoreVipView.swift | 10 + BeeReel/Delegate/AppDelegate.swift | 5 + BeeReel/Lib/IAP/BRIAP.swift | 202 ++++++++++++++++++ BeeReel/Lib/IAP/BRIAPOrderModel.swift | 34 +++ BeeReel/Lib/IAP/BRIAPVerifyModel.swift | 17 ++ BeeReel/Lib/IAP/BRWaitRestoreModel.swift | 45 ++++ BeeReel/Sources/Localizable.xcstrings | 80 +++++++ .../JXIAPManager/JXIAPManager.swift | 185 ++++++++++++++++ 16 files changed, 769 insertions(+), 5 deletions(-) create mode 100644 BeeReel/Class/Store/Model/BRRechargeRecordModel.swift create mode 100644 BeeReel/Lib/IAP/BRIAP.swift create mode 100644 BeeReel/Lib/IAP/BRIAPOrderModel.swift create mode 100644 BeeReel/Lib/IAP/BRIAPVerifyModel.swift create mode 100644 BeeReel/Lib/IAP/BRWaitRestoreModel.swift create mode 100644 BeeReel/Thirdparty/JXIAPManager/JXIAPManager.swift diff --git a/BeeReel.xcodeproj/project.pbxproj b/BeeReel.xcodeproj/project.pbxproj index 290e672..bacc938 100644 --- a/BeeReel.xcodeproj/project.pbxproj +++ b/BeeReel.xcodeproj/project.pbxproj @@ -209,6 +209,12 @@ F39855942E379D9600E2D28D /* BRVideoDetailRecommendCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39855932E379D9600E2D28D /* BRVideoDetailRecommendCell.swift */; }; F39855962E38A27500E2D28D /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = F39855952E38A27500E2D28D /* GoogleService-Info.plist */; }; F39855982E38BB3500E2D28D /* BRVideoDetailRecommendTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39855972E38BB3500E2D28D /* BRVideoDetailRecommendTransformer.swift */; }; + F398559C2E38CF9700E2D28D /* JXIAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F398559A2E38CF9700E2D28D /* JXIAPManager.swift */; }; + F398559F2E38D1FF00E2D28D /* BRIAPVerifyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F398559E2E38D1FF00E2D28D /* BRIAPVerifyModel.swift */; }; + F39855A12E38D22000E2D28D /* BRIAPOrderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39855A02E38D22000E2D28D /* BRIAPOrderModel.swift */; }; + F39855A32E38D25900E2D28D /* BRWaitRestoreModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39855A22E38D25900E2D28D /* BRWaitRestoreModel.swift */; }; + F39855A52E38D2A800E2D28D /* BRIAP.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39855A42E38D2A800E2D28D /* BRIAP.swift */; }; + F39855A72E38EE9800E2D28D /* BRRechargeRecordModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39855A62E38EE9800E2D28D /* BRRechargeRecordModel.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -429,6 +435,12 @@ F39855952E38A27500E2D28D /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; F39855972E38BB3500E2D28D /* BRVideoDetailRecommendTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRVideoDetailRecommendTransformer.swift; sourceTree = ""; }; F39855992E38CBE800E2D28D /* BeeReel.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BeeReel.entitlements; sourceTree = ""; }; + F398559A2E38CF9700E2D28D /* JXIAPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JXIAPManager.swift; sourceTree = ""; }; + F398559E2E38D1FF00E2D28D /* BRIAPVerifyModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRIAPVerifyModel.swift; sourceTree = ""; }; + F39855A02E38D22000E2D28D /* BRIAPOrderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRIAPOrderModel.swift; sourceTree = ""; }; + F39855A22E38D25900E2D28D /* BRWaitRestoreModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRWaitRestoreModel.swift; sourceTree = ""; }; + F39855A42E38D2A800E2D28D /* BRIAP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRIAP.swift; sourceTree = ""; }; + F39855A62E38EE9800E2D28D /* BRRechargeRecordModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRRechargeRecordModel.swift; sourceTree = ""; }; F70FA1F4169364C4C53534CE /* Pods-BeeReel.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BeeReel.release.xcconfig"; path = "Target Support Files/Pods-BeeReel/Pods-BeeReel.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -723,6 +735,7 @@ BF692AF62E0A480000A5C2DA /* Lib */ = { isa = PBXGroup; children = ( + F398559D2E38D1DC00E2D28D /* IAP */, F39855252E32294C00E2D28D /* Alert */, BF3A56862E30E0C2009E5CF9 /* Empty */, BF692B452E0A9B5800A5C2DA /* Player */, @@ -738,6 +751,7 @@ BF692AF72E0A480E00A5C2DA /* Thirdparty */ = { isa = PBXGroup; children = ( + F398559B2E38CF9700E2D28D /* JXIAPManager */, BF02B8372E30B2F800172177 /* AlignedCollectionViewFlowLayout */, BFC676A62E12AF04006659E5 /* FlowLayout */, BFC676612E0E2C8E006659E5 /* WMZBanner */, @@ -1185,6 +1199,7 @@ children = ( F398553D2E336D3000E2D28D /* BRPayDateModel.swift */, F39855532E34A49500E2D28D /* BRPayDataRequest.swift */, + F39855A62E38EE9800E2D28D /* BRRechargeRecordModel.swift */, ); path = Model; sourceTree = ""; @@ -1197,6 +1212,25 @@ path = Guide; sourceTree = ""; }; + F398559B2E38CF9700E2D28D /* JXIAPManager */ = { + isa = PBXGroup; + children = ( + F398559A2E38CF9700E2D28D /* JXIAPManager.swift */, + ); + path = JXIAPManager; + sourceTree = ""; + }; + F398559D2E38D1DC00E2D28D /* IAP */ = { + isa = PBXGroup; + children = ( + F39855A42E38D2A800E2D28D /* BRIAP.swift */, + F398559E2E38D1FF00E2D28D /* BRIAPVerifyModel.swift */, + F39855A02E38D22000E2D28D /* BRIAPOrderModel.swift */, + F39855A22E38D25900E2D28D /* BRWaitRestoreModel.swift */, + ); + path = IAP; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1358,6 +1392,8 @@ BF02B8202E2F9B1000172177 /* BRWebViewController.swift in Sources */, BF692B242E0A825B00A5C2DA /* BRCryptorService.swift in Sources */, BF02B8242E2FAEB500172177 /* BRAboutUsCell.swift in Sources */, + F39855A72E38EE9800E2D28D /* BRRechargeRecordModel.swift in Sources */, + F398559F2E38D1FF00E2D28D /* BRIAPVerifyModel.swift in Sources */, BF692B342E0A87C800A5C2DA /* UIDevice+BRAdd.swift in Sources */, BF692B3E2E0A8D2300A5C2DA /* BRTabBarController.swift in Sources */, BF02B7E12E2DE64200172177 /* BRVideoProgressView.swift in Sources */, @@ -1380,6 +1416,7 @@ BF02B7ED2E2E390500172177 /* BRScrollView.swift in Sources */, F39855562E34BD6000E2D28D /* BRWalletViewController.swift in Sources */, BF3A56812E30C08F009E5CF9 /* BRHotSearchTagCell.swift in Sources */, + F39855A12E38D22000E2D28D /* BRIAPOrderModel.swift in Sources */, BF02B8132E2F83C200172177 /* BRMineViewController.swift in Sources */, BF02B8222E2FAB1600172177 /* BRAboutUsViewController.swift in Sources */, BFC6766F2E0E3B5C006659E5 /* UIImageView+BRAdd.swift in Sources */, @@ -1410,6 +1447,7 @@ BF692AEC2E0A475D00A5C2DA /* SceneDelegate.swift in Sources */, BF692B492E0A9D0E00A5C2DA /* UIView+BRAdd.swift in Sources */, BF02B7F82E2F211A00172177 /* BRHomeCategoriesMainCell.swift in Sources */, + F398559C2E38CF9700E2D28D /* JXIAPManager.swift in Sources */, BF02B8192E2F8B1100172177 /* BRMineCell.swift in Sources */, BFC676812E122733006659E5 /* BRPlayerProtocol.swift in Sources */, BFC676BC2E138ABB006659E5 /* BRNewReleasesViewController.swift in Sources */, @@ -1459,6 +1497,7 @@ F39855892E37732600E2D28D /* BRGuideViewController.swift in Sources */, F39855782E375E7000E2D28D /* Dictionary+BRAdd.swift in Sources */, BFC676832E122CC5006659E5 /* BRPlayerViewModel.swift in Sources */, + F39855A52E38D2A800E2D28D /* BRIAP.swift in Sources */, BF02B7EF2E2E4BFD00172177 /* BRRateModel.swift in Sources */, BF692B672E0BC6C700A5C2DA /* AppDelegate+BRConfig.swift in Sources */, BF02B8022E2F39FE00172177 /* BRCategorieShortViewController.swift in Sources */, @@ -1514,6 +1553,7 @@ BF692B2A2E0A84F700A5C2DA /* JXUUID.m in Sources */, BF692B2B2E0A84F700A5C2DA /* PDKeyChain.m in Sources */, BF3A56852E30CA78009E5CF9 /* BRSearchResultCell.swift in Sources */, + F39855A32E38D25900E2D28D /* BRWaitRestoreModel.swift in Sources */, BF02B7FA2E2F225D00172177 /* BRHomeCategoryModel.swift in Sources */, BFC6766B2E0E395F006659E5 /* BRHomeHeaderBannerCell.swift in Sources */, F398557E2E3766A300E2D28D /* BRStatAPI.swift in Sources */, diff --git a/BeeReel/Base/Define/BRUserDefaultsKey.swift b/BeeReel/Base/Define/BRUserDefaultsKey.swift index 6d3a5fe..39cfa88 100644 --- a/BeeReel/Base/Define/BRUserDefaultsKey.swift +++ b/BeeReel/Base/Define/BRUserDefaultsKey.swift @@ -19,3 +19,5 @@ let kBRVideoRevolutionDefaultsKey = "kBRVideoRevolutionDefaultsKey" let kBRHasBeenOpenedAPPDefaultsKey = "kBRHasBeenOpenedAPPDefaultsKey" let kBRApnsAlertDefaultsKey = "kBRApnsAlertDefaultsKey" + +let kBRWaitRestoreIAPDefaultsKey = "kBRWaitRestoreIAPDefaultsKey" diff --git a/BeeReel/Base/Network/API/BRStoreAPI.swift b/BeeReel/Base/Network/API/BRStoreAPI.swift index 3c35f28..6284106 100644 --- a/BeeReel/Base/Network/API/BRStoreAPI.swift +++ b/BeeReel/Base/Network/API/BRStoreAPI.swift @@ -26,4 +26,68 @@ class BRStoreAPI { completer?(response.data) } } + + ///创建内购订单 + static func requestCreateOrder(payId: String, shortPlayId: String, videoId: String, isDiscount: Bool = false, completer: ((_ orderModel: BRIAPOrderModel?) -> Void)?) { + var param = BRNetworkParameters(path: "/createOrder") + param.isToast = false + param.parameters = [ + "payment_channel" : "apple", + "short_play_id" : shortPlayId, + "video_id" : videoId, + "pay_setting_id" : payId, + "is_discount" : isDiscount ? 1 : 0 + ] + + BRNetwork.request(parameters: param) { (response: BRNetworkResponse) in + guard let data = response.data else { + BRToast.show(text: "beereel_network".localized) + completer?(nil) + return + } + + if let message = data.message, message.count > 0 { + if response.data?.code == 30007 { + BRToast.show(text: "beereel_vip_error_1".localized) + } else { + BRToast.show(text: message) + } + + completer?(nil) + } else { + completer?(data) + } + } + } + + ///校验内购 + static func requestVerifyOrder(orderCode: String, payId: String, productId: String, purchaseToken: String, completer: ((_ model: BRIAPVerifyModel?) -> Void)?) { + var param = BRNetworkParameters(path: "/applePaid") + param.parameters = [ + "order_code" : orderCode, + "pay_setting_id" : payId, + "pkg_name" : kBRAPPBundleIdentifier, + "transaction_id" : productId, + "purchases_token" : purchaseToken + ] + + BRNetwork.request(parameters: param) { (response: BRNetworkResponse) in + completer?(response.data) + } + } + + ///充值记录 + static func requestRechargeRecord(buyType: BuyType, page: Int, completer: ((_ listModel: BRListModel?) -> Void)?) { + var param = BRNetworkParameters(path: "/getCustomerOrder") + param.method = .get + param.parameters = [ + "page_size" : 20, + "current_page" : page, + "buy_type" : buyType == .subVip ? "vip" : buyType.rawValue + ] + + BRNetwork.request(parameters: param) { (response: BRNetworkResponse>) in + completer?(response.data) + } + } } diff --git a/BeeReel/Class/Store/Controller/BRCoinOrderRecordViewController.swift b/BeeReel/Class/Store/Controller/BRCoinOrderRecordViewController.swift index ff628c6..8a9770c 100644 --- a/BeeReel/Class/Store/Controller/BRCoinOrderRecordViewController.swift +++ b/BeeReel/Class/Store/Controller/BRCoinOrderRecordViewController.swift @@ -9,6 +9,10 @@ import UIKit class BRCoinOrderRecordViewController: BRViewController, WMZPageProtocol { + + private lazy var listArr: [BRRechargeRecordModel] = [] + private lazy var page = 1 + private lazy var collectionViewLayout: UICollectionViewFlowLayout = { let layout = UICollectionViewFlowLayout() layout.itemSize = .init(width: UIScreen.width - 30, height: 80) @@ -34,6 +38,8 @@ class BRCoinOrderRecordViewController: BRViewController, WMZPageProtocol { self.view.backgroundColor = .colorFFFFFF() br_setupUI() + + requestDataList(page: 1, completer: nil) } func getMyScrollView() -> UIScrollView { @@ -41,11 +47,15 @@ class BRCoinOrderRecordViewController: BRViewController, WMZPageProtocol { } override func handleHeaderRefresh(_ completer: (() -> Void)?) { - completer?() + self.requestDataList(page: 1) { + completer?() + } } override func handleFooterRefresh(_ completer: (() -> Void)?) { - self.collectionView.br_endFooterRefreshing() + self.requestDataList(page: self.page + 1) { [weak self] in + self?.collectionView.br_endFooterRefreshing() + } } } @@ -70,11 +80,32 @@ extension BRCoinOrderRecordViewController: UICollectionViewDelegate, UICollectio func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! BRCoinOrderRecordCell + cell.model = self.listArr[indexPath.row] return cell } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return 10 + return self.listArr.count + } + +} + +extension BRCoinOrderRecordViewController { + + private func requestDataList(page: Int, completer: (() -> Void)?) { + BRStoreAPI.requestRechargeRecord(buyType: .coins, page: page) { [weak self] listModel in + guard let self = self else { return } + + if let list = listModel?.list { + if page == 1 { + self.listArr.removeAll() + } + self.listArr += list + self.page = page + self.collectionView.reloadData() + } + completer?() + } } } diff --git a/BeeReel/Class/Store/Controller/BRStoreViewController.swift b/BeeReel/Class/Store/Controller/BRStoreViewController.swift index 69ecace..e99e2b1 100644 --- a/BeeReel/Class/Store/Controller/BRStoreViewController.swift +++ b/BeeReel/Class/Store/Controller/BRStoreViewController.swift @@ -59,6 +59,18 @@ class BRStoreViewController: BRViewController { self.statusBarStyle = .lightContent self.view.backgroundColor = .color1C1C1C() + + + + let barButtonItem = UIBarButtonItem(title: "Restore".localized, style: .plain, target: self, action: #selector(handleRestore)) + var titleTextAttributes = barButtonItem.titleTextAttributes(for: .normal) ?? [:] + titleTextAttributes[.foregroundColor] = UIColor.white + titleTextAttributes[.font] = UIFont.fontRegular(ofSize: 14) + barButtonItem.setTitleTextAttributes(titleTextAttributes, for: .normal) + self.navigationItem.rightBarButtonItem = barButtonItem + + + configNavigationBack("nav_back_icon_03") br_setupUI() @@ -71,7 +83,14 @@ class BRStoreViewController: BRViewController { self.navigationController?.setNavigationBarHidden(false, animated: true) br_setNavigation(style: .dark) } - + + @objc private func handleRestore() { + BRIAP.manager.restore { isFinish in + guard isFinish else { return } + BRLoginManager.manager.updateUserInfo(completer: nil) + } + + } } extension BRStoreViewController { diff --git a/BeeReel/Class/Store/Model/BRRechargeRecordModel.swift b/BeeReel/Class/Store/Model/BRRechargeRecordModel.swift new file mode 100644 index 0000000..b138623 --- /dev/null +++ b/BeeReel/Class/Store/Model/BRRechargeRecordModel.swift @@ -0,0 +1,15 @@ +// +// BRRechargeRecordModel.swift +// BeeReel +// +// Created by 长沙鸿瑶 on 2025/7/29. +// + +import UIKit +import SmartCodable + +class BRRechargeRecordModel: BRModel, SmartCodable { + var type: String? + var created_at: String? + var value: String? +} diff --git a/BeeReel/Class/Store/View/BRCoinOrderRecordCell.swift b/BeeReel/Class/Store/View/BRCoinOrderRecordCell.swift index 3ce4ea1..1bacb82 100644 --- a/BeeReel/Class/Store/View/BRCoinOrderRecordCell.swift +++ b/BeeReel/Class/Store/View/BRCoinOrderRecordCell.swift @@ -9,6 +9,12 @@ import UIKit class BRCoinOrderRecordCell: BRCollectionViewCell { + var model: BRRechargeRecordModel? { + didSet { + + } + } + private lazy var titleLabel: UILabel = { let label = UILabel() label.font = .fontRegular(ofSize: 14) diff --git a/BeeReel/Class/Store/View/BRStoreCoinView.swift b/BeeReel/Class/Store/View/BRStoreCoinView.swift index b117794..ae94ff9 100644 --- a/BeeReel/Class/Store/View/BRStoreCoinView.swift +++ b/BeeReel/Class/Store/View/BRStoreCoinView.swift @@ -135,7 +135,16 @@ extension BRStoreCoinView: UICollectionViewDelegate, UICollectionViewDataSource return cell } - + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let item = self.newList[indexPath.section][indexPath.row] + + BRIAP.manager.start(model: item, shortPlayId: shortPlayId, videoId: videoId) { [weak self] finish in + if finish { + BRLoginManager.manager.updateUserInfo(completer: nil) + self?.buyFinishBlock?() + } + } + } diff --git a/BeeReel/Class/Store/View/BRStoreVipView.swift b/BeeReel/Class/Store/View/BRStoreVipView.swift index 4220c8e..96c9720 100644 --- a/BeeReel/Class/Store/View/BRStoreVipView.swift +++ b/BeeReel/Class/Store/View/BRStoreVipView.swift @@ -104,5 +104,15 @@ extension BRStoreVipView: UICollectionViewDelegate, UICollectionViewDataSource { return cell } + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let item = list[indexPath.row] + + BRIAP.manager.start(model: item, shortPlayId: shortPlayId, videoId: videoId) { [weak self] finish in + if finish { + BRLoginManager.manager.updateUserInfo(completer: nil) + self?.buyFinishBlock?() + } + } + } } diff --git a/BeeReel/Delegate/AppDelegate.swift b/BeeReel/Delegate/AppDelegate.swift index 5e1bd03..680e7ae 100644 --- a/BeeReel/Delegate/AppDelegate.swift +++ b/BeeReel/Delegate/AppDelegate.swift @@ -25,6 +25,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { requestAPNS() + BRIAP.manager.restore(isLoding: false) { isFinish in + guard isFinish else { return } + BRLoginManager.manager.updateUserInfo(completer: nil) + } + return true } diff --git a/BeeReel/Lib/IAP/BRIAP.swift b/BeeReel/Lib/IAP/BRIAP.swift new file mode 100644 index 0000000..1d7fa12 --- /dev/null +++ b/BeeReel/Lib/IAP/BRIAP.swift @@ -0,0 +1,202 @@ +// +// BRIAP.swift +// BeeReel +// +// Created by 长沙鸿瑶 on 2025/7/29. +// + +import UIKit + +class BRIAP { + typealias CompletionHandler = ((_ finish: Bool) -> Void) + ///内购模版前缀 + static let IAPPrefix = "veloria." + + + static let manager = BRIAP() + + ///成功回调 + private var completionHandler: CompletionHandler? + + private lazy var iapManager: JXIAPManager = { + let manager = JXIAPManager() + manager.delegate = self + return manager + }() + + private var orderCode: String? + private var payId: String? + + ///恢复购买使用 + ///等待恢复的数据 + private var waitRestoreModel: BRWaitRestoreModel? = UserDefaults.br_object(forKey: kBRWaitRestoreIAPDefaultsKey, as: BRWaitRestoreModel.self) + + + + ///开始内购 + func start(model: BRPayItem, shortPlayId: String? = nil, videoId: String? = nil, isDiscount: Bool = false, hudShowView: UIView? = nil, handler: CompletionHandler? = nil) { + + if let _ = self.waitRestoreModel { + BRToast.show(text: "beereel_pay_error_1".localized) + handler?(false) + return + } + + guard let payId = model.id else { + handler?(false) + return + } + self.completionHandler = handler + self.waitRestoreModel = BRWaitRestoreModel() + self.waitRestoreModel?.buyType = model.buy_type + + let productId = BRIAP.IAPPrefix + (model.ios_template_id ?? "") + + BRHUD.show(containerView: hudShowView) + + BRStoreAPI.requestCreateOrder(payId: payId, shortPlayId: shortPlayId ?? "0", videoId: videoId ?? "0", isDiscount: isDiscount) { orderModel in + guard let orderModel = orderModel else { + BRHUD.dismiss() + self.waitRestoreModel = nil + self.completionHandler?(false) + return + } + self.orderCode = orderModel.order_code + self.payId = payId + self.waitRestoreModel?.payId = payId + self.waitRestoreModel?.orderCode = orderModel.order_code + + + self.iapManager.start(productId: productId, orderId: self.orderCode ?? "", discount: nil) + + } + + } + + func restore(isLoding: Bool = true, completer: ((_ isFinish: Bool) -> Void)?) { + guard let waitRestoreModel = self.waitRestoreModel, + let orderCode = waitRestoreModel.orderCode, + let payId = waitRestoreModel.payId, + let productId = waitRestoreModel.productId, + let receipt = waitRestoreModel.receipt + else { + if isLoding { + BRToast.show(text: "beereel_pay_error_3".localized) + } + return + } + + if isLoding { + BRHUD.show() + } + BRStoreAPI.requestVerifyOrder(orderCode: orderCode, payId: payId, productId: productId, purchaseToken: receipt) { model in + if isLoding { + BRHUD.dismiss() + } + + guard let model = model else { + completer?(false) + return + } + let buyType = self.waitRestoreModel?.buyType + self.waitRestoreModel = nil + UserDefaults.br_setObject(self.waitRestoreModel, forKey: kBRWaitRestoreIAPDefaultsKey) + + if model.status == "success" { + if buyType == .subVip { + BRLoginManager.manager.userInfo?.is_vip = true + } + + if isLoding { + BRToast.show(text: "beereel_succeed".localized) + } + completer?(true) + if buyType == .subVip { + NotificationCenter.default.post(name: BRIAP.buyVipFinishNotification, object: nil) + } + } else { + completer?(false) + } + } + + } + + + func getProductId(templateId: String?) -> String? { + guard let templateId = templateId else { return nil } + return BRIAP.IAPPrefix + templateId + } +} + +//MARK: -------------- JXIAPManagerDelegate -------------- +extension BRIAP: JXIAPManagerDelegate { + + func jx_iapPaySuccess(productId: String, receipt: String, transactionIdentifier: String?) { + guard let orderCode = self.orderCode, let payId = self.payId else { + self.orderCode = nil + self.payId = nil + self.waitRestoreModel = nil + self.completionHandler?(false) + return + } + + self.waitRestoreModel?.productId = productId + self.waitRestoreModel?.receipt = receipt + + UserDefaults.br_setObject(self.waitRestoreModel, forKey: kBRWaitRestoreIAPDefaultsKey) + + BRStoreAPI.requestVerifyOrder(orderCode: orderCode, payId: payId, productId: productId, purchaseToken: receipt) { model in + BRHUD.dismiss() + + self.orderCode = nil + self.payId = nil + + guard let model = model else { + self.completionHandler?(false) + return + } + + let buyType = self.waitRestoreModel?.buyType + self.waitRestoreModel = nil + UserDefaults.br_setObject(self.waitRestoreModel, forKey: kBRWaitRestoreIAPDefaultsKey) + + if model.status == "success" { + if buyType == .subVip { + BRLoginManager.manager.userInfo?.is_vip = true + } + + BRToast.show(text: "beereel_succeed".localized) + self.completionHandler?(true) + if buyType == .subVip { + NotificationCenter.default.post(name: BRIAP.buyVipFinishNotification, object: nil) + } + } else { + self.completionHandler?(false) + } + } + + } + + func jx_iapPayFailed(productId: String, code: JXIAPManagerCode) { + self.orderCode = nil + self.payId = nil + self.waitRestoreModel = nil + + BRHUD.dismiss() + + if code == .noProduct { + BRToast.show(text: "beereel_pay_error_2".localized) + } else if code == .cancelled { + BRToast.show(text: "beereel_pay_error_4".localized) + } + self.completionHandler?(false) + } + + +} + +extension BRIAP { + ///成功购买会员 + @objc static let buyVipFinishNotification = NSNotification.Name(rawValue: "BRIAP.buyVipFinishNotification") + +} diff --git a/BeeReel/Lib/IAP/BRIAPOrderModel.swift b/BeeReel/Lib/IAP/BRIAPOrderModel.swift new file mode 100644 index 0000000..aefe7ce --- /dev/null +++ b/BeeReel/Lib/IAP/BRIAPOrderModel.swift @@ -0,0 +1,34 @@ +// +// BRIAPOrderModel.swift +// BeeReel +// +// Created by 长沙鸿瑶 on 2025/7/29. +// + +import UIKit +import SmartCodable + +class BRIAPOrderModel: BRModel, SmartCodable { + + var code: Int? + var message: String? + var money: String? + var order_code: String? + var is_backhaul: String? + var discount: BRIAPOrderDiscountModel? +} + +class BRIAPOrderDiscountModel: BRModel, SmartCodable { + + var is_discount: Bool? + var discount_code: String? + var sign_data: BRIAPOrderDiscountSign? +} + +class BRIAPOrderDiscountSign: BRModel, SmartCodable { + + var keyIdentifier: String? + var nonce: String? + var timestamp: TimeInterval? + var signature: String? +} diff --git a/BeeReel/Lib/IAP/BRIAPVerifyModel.swift b/BeeReel/Lib/IAP/BRIAPVerifyModel.swift new file mode 100644 index 0000000..5d48aac --- /dev/null +++ b/BeeReel/Lib/IAP/BRIAPVerifyModel.swift @@ -0,0 +1,17 @@ +// +// BRIAPVerifyModel.swift +// BeeReel +// +// Created by 长沙鸿瑶 on 2025/7/29. +// + +import UIKit +import SmartCodable + +class BRIAPVerifyModel: BRModel, SmartCodable { + + var code: String? + var status: String? + var money: String? + var is_backhaul: String? +} diff --git a/BeeReel/Lib/IAP/BRWaitRestoreModel.swift b/BeeReel/Lib/IAP/BRWaitRestoreModel.swift new file mode 100644 index 0000000..8b6c018 --- /dev/null +++ b/BeeReel/Lib/IAP/BRWaitRestoreModel.swift @@ -0,0 +1,45 @@ +// +// BRWaitRestoreModel.swift +// BeeReel +// +// Created by 长沙鸿瑶 on 2025/7/29. +// + +import UIKit + +class BRWaitRestoreModel: BRModel, NSSecureCoding { + + var orderCode: String? + var payId: String? + var productId: String? + var receipt: String? + var buyType: BRStoreAPI.BuyType? + + + required init() { } + + static var supportsSecureCoding: Bool { + get { + return true + } + } + + func encode(with coder: NSCoder) { + coder.encode(orderCode, forKey: "orderCode") + coder.encode(payId, forKey: "payId") + coder.encode(productId, forKey: "productId") + coder.encode(receipt, forKey: "receipt") + coder.encode(buyType?.rawValue, forKey: "buyType") + } + + required init?(coder: NSCoder) { + super.init() + orderCode = coder.decodeObject(of: NSString.self, forKey: "orderCode") as? String + payId = coder.decodeObject(of: NSString.self, forKey: "payId") as? String + productId = coder.decodeObject(of: NSString.self, forKey: "productId") as? String + receipt = coder.decodeObject(of: NSString.self, forKey: "receipt") as? String + if let type = coder.decodeObject(of: NSString.self, forKey: "buyType") as? String { + buyType = BRStoreAPI.BuyType(rawValue: type) + } + } +} diff --git a/BeeReel/Sources/Localizable.xcstrings b/BeeReel/Sources/Localizable.xcstrings index 727f30c..82d29f0 100644 --- a/BeeReel/Sources/Localizable.xcstrings +++ b/BeeReel/Sources/Localizable.xcstrings @@ -102,6 +102,63 @@ } } }, + "beereel_pay_error_1" : { + "comment" : "还有未完成购买", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You have unfinished in-app purchases, please restore them first." + } + } + } + }, + "beereel_pay_error_2" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invalid in-app purchase" + } + } + } + }, + "beereel_pay_error_3" : { + "comment" : "没有可恢复购买", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No in-app purchases can be restored" + } + } + } + }, + "beereel_pay_error_4" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Payment has been cancelled" + } + } + } + }, + "beereel_succeed" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Success" + } + } + } + }, "beereel_video_lock_tip_text" : { "comment" : "请解锁上一集", "extractionState" : "manual", @@ -114,6 +171,18 @@ } } }, + "beereel_vip_error_1" : { + "comment" : "已是会员", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You are already a member!" + } + } + } + }, "Browse Genres" : { "extractionState" : "manual", "localizations" : { @@ -631,6 +700,17 @@ } } }, + "Restore" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restore" + } + } + } + }, "Reward Coins" : { "extractionState" : "manual", "localizations" : { diff --git a/BeeReel/Thirdparty/JXIAPManager/JXIAPManager.swift b/BeeReel/Thirdparty/JXIAPManager/JXIAPManager.swift new file mode 100644 index 0000000..0825ca8 --- /dev/null +++ b/BeeReel/Thirdparty/JXIAPManager/JXIAPManager.swift @@ -0,0 +1,185 @@ +// +// JXIAPManager.swift +// BoJia +// +// Created by 火山传媒 on 2024/6/3. +// + +import UIKit +import StoreKit + +@objc protocol JXIAPManagerDelegate { + /// 购买成功 + @objc optional func jx_iapPaySuccess(productId: String, receipt: String, transactionIdentifier: String?) + /// 购买失败 + @objc optional func jx_iapPayFailed(productId: String, code: JXIAPManagerCode) + /// 恢复商品(仅限永久有效商品) + @objc optional func iapPayRestore(productIds: [String], transactionIds: [String]) +// /// 加载 +// @objc optional func iapPayShowHud() +// /// 系统错误 +// @objc optional func iapSysWrong() +// /// 验证成功 +// @objc optional func verifySuccess() +// /// 验证失败 +// @objc optional func verifyFailed() +} + +@objc enum JXIAPManagerCode: Int { + ///未知错误 + case unknown + ///取消交易 + case cancelled + ///没有商品 + case noProduct +} + +class JXIAPManager: NSObject { + + + static let manager: JXIAPManager = JXIAPManager() + + weak var delegate: JXIAPManagerDelegate? + + private var payment: SKPayment? + + private var product: SKProduct? + private var productId: String? + + private var discount: SKPaymentDiscount? + private var orderId: String? + private var applicationUsername: String? { + get { + return orderId + } + } + + + override init() { + super.init() + SKPaymentQueue.default().add(self) + } + + + func start(productId: String, orderId: String, discount: SKPaymentDiscount? = nil) { + self.product = nil + self.productId = productId + self.orderId = orderId + self.discount = discount + + let set = Set([productId]) + let productsRequest = SKProductsRequest(productIdentifiers: set) + productsRequest.delegate = self + productsRequest.start() + } + + /// 购买商品 + private func buyProduct() { + guard let product = self.product else { return } + + // 要购买商品,开个小票 + let payment = SKMutablePayment(product: product) + payment.applicationUsername = applicationUsername + if let discount = self.discount { + payment.paymentDiscount = discount + self.discount = nil + } + + self.payment = payment + + // 去收银台排队,准备购买 + SKPaymentQueue.default().add(payment) + } + +} + +//MARK: -------------- SKProductsRequestDelegate -------------- +extension JXIAPManager: SKProductsRequestDelegate { + func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { + guard let product = response.products.first else { + DispatchQueue.main.async { + if let productId = self.productId { + self.productId = nil + self.delegate?.jx_iapPayFailed?(productId: productId, code: .noProduct) + } + } + return + } + self.product = product + + self.buyProduct() + + } +} + +//MARK: -------------- SKPaymentTransactionObserver -------------- +extension JXIAPManager: SKPaymentTransactionObserver { + ///购买回调 + func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { + + for transaction in transactions { + switch transaction.transactionState { + case .purchased: + DispatchQueue.main.async { + self.completeTransaction(transaction: transaction) + } + SKPaymentQueue.default().finishTransaction(transaction) + + case .failed: + DispatchQueue.main.async { + self.failedTransaction(transaction: transaction) + } + SKPaymentQueue.default().finishTransaction(transaction) +// case .restored: +// self.restoreTransaction(transaction: transaction) + + case .purchasing: + break + default: + SKPaymentQueue.default().finishTransaction(transaction) + break + } + + + } + } + +// func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool { +// return true +// } + + /// 恢复购买回调 + func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) { + + } + +} + +extension JXIAPManager { + private func completeTransaction(transaction: SKPaymentTransaction) { + guard let receiptURL = Bundle.main.appStoreReceiptURL else { return } + let receiptData = NSData(contentsOf: receiptURL) + guard let encodeStr = receiptData?.base64EncodedString(options: .endLineWithLineFeed) else { return } + guard let transactionIdentifier = transaction.transactionIdentifier else { return } + + + guard let productId = self.productId, productId == transaction.payment.productIdentifier else { return } + self.productId = nil + self.delegate?.jx_iapPaySuccess?(productId: productId, receipt: encodeStr, transactionIdentifier: transactionIdentifier) + + } + + private func failedTransaction(transaction: SKPaymentTransaction) { + let error = transaction.error as? SKError + guard let productId = self.productId else { return } + self.productId = nil + + switch error?.code { + case SKError.paymentCancelled: + self.delegate?.jx_iapPayFailed?(productId: productId, code: .cancelled) + default: + self.delegate?.jx_iapPayFailed?(productId: productId, code: .unknown) + } + + } +}