内购功能开发,配置内购商品

This commit is contained in:
zjx 2025-06-05 16:08:41 +08:00
parent 6d690cc471
commit 337c25baaf
19 changed files with 722 additions and 5 deletions

View File

@ -208,6 +208,11 @@
BFF5B25A2DF13BE50044227A /* VPGiveCoinRecordsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF5B2592DF13BE50044227A /* VPGiveCoinRecordsCell.swift */; };
BFF5B25C2DF13F850044227A /* Date+VPAdd.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF5B25B2DF13F850044227A /* Date+VPAdd.swift */; };
BFF5B25E2DF1423F0044227A /* VPWalletBaseCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF5B25D2DF1423F0044227A /* VPWalletBaseCell.swift */; };
BFF5B2612DF16B430044227A /* JXIAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF5B25F2DF16B430044227A /* JXIAPManager.swift */; };
BFF5B2642DF16C380044227A /* VPIAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF5B2632DF16C380044227A /* VPIAPManager.swift */; };
BFF5B2662DF16CF60044227A /* VPWaitRestoreModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF5B2652DF16CF60044227A /* VPWaitRestoreModel.swift */; };
BFF5B2682DF16EA30044227A /* VPIAPOrderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF5B2672DF16EA30044227A /* VPIAPOrderModel.swift */; };
BFF5B26A2DF170DD0044227A /* VPIAPVerifyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF5B2692DF170DD0044227A /* VPIAPVerifyModel.swift */; };
F939C04AD4003BA127F15C28 /* Pods_Veloria.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34F57E87E765BF8D72A43DCA /* Pods_Veloria.framework */; };
/* End PBXBuildFile section */
@ -421,6 +426,11 @@
BFF5B2592DF13BE50044227A /* VPGiveCoinRecordsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPGiveCoinRecordsCell.swift; sourceTree = "<group>"; };
BFF5B25B2DF13F850044227A /* Date+VPAdd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+VPAdd.swift"; sourceTree = "<group>"; };
BFF5B25D2DF1423F0044227A /* VPWalletBaseCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPWalletBaseCell.swift; sourceTree = "<group>"; };
BFF5B25F2DF16B430044227A /* JXIAPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JXIAPManager.swift; sourceTree = "<group>"; };
BFF5B2632DF16C380044227A /* VPIAPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPIAPManager.swift; sourceTree = "<group>"; };
BFF5B2652DF16CF60044227A /* VPWaitRestoreModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPWaitRestoreModel.swift; sourceTree = "<group>"; };
BFF5B2672DF16EA30044227A /* VPIAPOrderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPIAPOrderModel.swift; sourceTree = "<group>"; };
BFF5B2692DF170DD0044227A /* VPIAPVerifyModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPIAPVerifyModel.swift; sourceTree = "<group>"; };
E0BDA3570E00C90877E45AA0 /* Pods-VideoPlayer.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-VideoPlayer.debug.xcconfig"; path = "Target Support Files/Pods-VideoPlayer/Pods-VideoPlayer.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -498,6 +508,7 @@
1B056E352DDAC1DE007EE38D /* Libs */ = {
isa = PBXGroup;
children = (
BFF5B2622DF16BE10044227A /* VPIAPManager */,
BFF5B2352DF013020044227A /* Alert */,
BF5E75D92DE5B89300DE9DFE /* MarqueeView */,
BF5E75B42DE46D9500DE9DFE /* Empty */,
@ -715,6 +726,7 @@
BF0FA6E82DDC5F6F00C9E5F2 /* Thirdparty */ = {
isa = PBXGroup;
children = (
BFF5B2602DF16B430044227A /* JXIAPManager */,
BF5E75CA2DE5692D00DE9DFE /* JXTransition */,
BF0FA7932DE16E9300C9E5F2 /* JXTagView */,
BF0FA6ED2DDC5F8700C9E5F2 /* JXUUID */,
@ -1174,6 +1186,25 @@
path = Alert;
sourceTree = "<group>";
};
BFF5B2602DF16B430044227A /* JXIAPManager */ = {
isa = PBXGroup;
children = (
BFF5B25F2DF16B430044227A /* JXIAPManager.swift */,
);
path = JXIAPManager;
sourceTree = "<group>";
};
BFF5B2622DF16BE10044227A /* VPIAPManager */ = {
isa = PBXGroup;
children = (
BFF5B2632DF16C380044227A /* VPIAPManager.swift */,
BFF5B2652DF16CF60044227A /* VPWaitRestoreModel.swift */,
BFF5B2672DF16EA30044227A /* VPIAPOrderModel.swift */,
BFF5B2692DF170DD0044227A /* VPIAPVerifyModel.swift */,
);
path = VPIAPManager;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -1321,6 +1352,7 @@
BF0FA6DC2DDC5CD700C9E5F2 /* VPTokenModel.swift in Sources */,
BF0FA7572DDF159B00C9E5F2 /* VPExploreViewController.swift in Sources */,
1B056E772DDB3641007EE38D /* VPTabBarItemNormalVew.swift in Sources */,
BFF5B2662DF16CF60044227A /* VPWaitRestoreModel.swift in Sources */,
1B056E4B2DDAC6BA007EE38D /* VPDefine.swift in Sources */,
BFF5B2302DEFEF0C0044227A /* VPDeleteAccountNormalView.swift in Sources */,
1B056E702DDB019B007EE38D /* VPTabBarItemContainer.swift in Sources */,
@ -1359,6 +1391,7 @@
BFF5AFA42DE6F15E0044227A /* VPMeVipCell.swift in Sources */,
BFF5AFB02DE7F9A80044227A /* VPVipViewController.swift in Sources */,
BF0FA75D2DDF208400C9E5F2 /* VPExplorePlayerControlView.swift in Sources */,
BFF5B2682DF16EA30044227A /* VPIAPOrderModel.swift in Sources */,
BFF5B21C2DEEDE130044227A /* VPWebViewController+Script.swift in Sources */,
BF5E75CB2DE5692D00DE9DFE /* UINavigationController+JXTransition.swift in Sources */,
BF5E75CC2DE5692D00DE9DFE /* JXTransitionDefine.swift in Sources */,
@ -1429,6 +1462,7 @@
BFF5AFA82DE704DC0044227A /* VPMeCoinCell.swift in Sources */,
BFF5AFAE2DE717BB0044227A /* VPVipPageViewController.swift in Sources */,
BFF5B2502DF12FBC0044227A /* VPConsumptionRecordsViewController.swift in Sources */,
BFF5B2642DF16C380044227A /* VPIAPManager.swift in Sources */,
BF0FA70E2DDC6ACC00C9E5F2 /* VPHomeItemContentCell.swift in Sources */,
BF0FA6D72DDC5BE100C9E5F2 /* VPURLPath.swift in Sources */,
1B056E5B2DDACD80007EE38D /* UIColor+VPAdd.swift in Sources */,
@ -1470,11 +1504,13 @@
BFF5AFAC2DE70CE20044227A /* VPMeToolCell.swift in Sources */,
BF5E75B32DE465EC00DE9DFE /* Dictionary+SPAdd.swift in Sources */,
BF0FA7162DDC78FF00C9E5F2 /* ZKCycleScrollViewFlowLayout.swift in Sources */,
BFF5B2612DF16B430044227A /* JXIAPManager.swift in Sources */,
BF0FA7172DDC78FF00C9E5F2 /* ZKCycleScrollView.swift in Sources */,
BF0FA7612DDFFE7100C9E5F2 /* VPVideoDetailModel.swift in Sources */,
BFF5AFD22DE9A58A0044227A /* VPVIPRecordViewController.swift in Sources */,
BFF5AFDA2DEE90350044227A /* VPVideoLockView.swift in Sources */,
BF5E75B82DE46F7100DE9DFE /* VPNetworkReachabilityManager.swift in Sources */,
BFF5B26A2DF170DD0044227A /* VPIAPVerifyModel.swift in Sources */,
BF0FA6D52DDC5B5D00C9E5F2 /* VPApi.swift in Sources */,
BF0FA7C12DE45D5D00C9E5F2 /* VPUserInfo.swift in Sources */,
BF0FA7392DDECF8900C9E5F2 /* VPHomeListViewController.swift in Sources */,

View File

@ -14,6 +14,27 @@ extension AppDelegate {
UIView.vp_Awake()
VPToast.config()
congifNavigation()
}
}
extension AppDelegate {
private func congifNavigation() {
let barButtonItem = UIBarButtonItem.appearance()
barButtonItem.setTitleTextAttributes([
.foregroundColor : UIColor.colorFFFFFF(),
], for: .normal)
barButtonItem.setTitleTextAttributes([
.foregroundColor : UIColor.colorFFFFFF()
], for: .highlighted)
}
}

View File

@ -21,6 +21,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
VPLoginManager.manager.updateUserInfo(completer: nil)
let _ = JXIAPManager.manager
self.registThirdparty(application, didFinishLaunchingWithOptions: launchOptions)
return true

View File

@ -15,3 +15,6 @@ let kVPLoginTokenDefaultsKey = "kVPLoginTokenDefaultsKey"
///
let kVPLoginUserInfoDefaultsKey = "kSPLoginUserInfoDefaultsKey"
///
let kVPWaitRestoreIAPDefaultsKey = "kVPWaitRestoreIAPDefaultsKey"

View File

@ -20,6 +20,21 @@ class VPWalletAPI {
param.method = .get
VPNetwork.request(parameters: param) { (response: VPNetworkResponse<VPPayTemplateModel>) in
/*
if let data = response.data {
var vipList: [VPPayTemplateItem] = []
data.list_sub_vip?.forEach({
if $0.vip_type_key == .quarter {
vipList.append($0)
}
})
data.list_sub_vip = vipList
completer?(data)
} else {
completer?(nil)
}
*/
completer?(response.data)
}
}
@ -80,4 +95,46 @@ class VPWalletAPI {
completer?(response.data)
}
}
///
static func requestCreateOrder(payId: String, shortPlayId: String, videoId: String, completer: ((_ orderModel: VPIAPOrderModel?) -> Void)?) {
var param = VPNetworkParameters(path: "/createOrder")
param.isToast = false
param.parameters = [
"payment_channel" : "apple",
"short_play_id" : shortPlayId,
"video_id" : videoId,
"pay_setting_id" : payId
]
VPNetwork.request(parameters: param) { (response: VPNetworkResponse<VPIAPOrderModel>) in
if let message = response.data?.message, message.count > 0 {
if response.data?.code == 30007 {
VPToast.show(text: "kVipToast01".localized)
} else {
VPToast.show(text: message)
}
completer?(nil)
} else {
completer?(response.data)
}
}
}
///
static func requestVerifyOrder(orderCode: String, payId: String, productId: String, purchaseToken: String, completer: ((_ model: VPIAPVerifyModel?) -> Void)?) {
var param = VPNetworkParameters(path: "/applePaid")
param.parameters = [
"order_code" : orderCode,
"pay_setting_id" : payId,
"pkg_name" : kVPAPPBundleIdentifier,
"transaction_id" : productId,
"purchases_token" : purchaseToken
]
VPNetwork.request(parameters: param) { (response: VPNetworkResponse<VPIAPVerifyModel>) in
completer?(response.data)
}
}
}

View File

@ -40,10 +40,16 @@ class VPDetailPlayerViewController: VPVideoPlayerViewController {
///
private weak var episodeView: VPEpisodeView?
deinit {
NotificationCenter.default.removeObserver(self)
}
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
self.dataSource = self
NotificationCenter.default.addObserver(self, selector: #selector(buyVipFinishNotification), name: VPIAPManager.buyVipFinishNotification, object: nil)
requestDetailData()
vp_setupUI()
@ -157,10 +163,25 @@ extension VPDetailPlayerViewController {
///
private func onRecharge() {
guard let videoInfo = self.viewModel.currentPlayer?.videoInfo else { return }
let view = VPPlayerRechargeView()
view.shortPlayId = videoInfo.short_play_id
view.videoId = videoInfo.short_play_video_id
view.present(in: nil)
}
///
@objc private func buyVipFinishNotification() {
guard VPLoginManager.manager.userInfo?.is_vip == true else { return }
self.detailModel?.episodeList?.forEach({
$0.is_lock = false
})
self.reloadData { [weak self] in
self?.play()
}
}
}

View File

@ -16,6 +16,9 @@ class VPPlayerCoinBuyView: UIView {
}
}
var shortPlayId: String?
var videoId: String?
private lazy var selectedIndex = 0
private lazy var collectionViewLayout: UICollectionViewFlowLayout = {
@ -100,6 +103,12 @@ extension VPPlayerCoinBuyView: UICollectionViewDelegate, UICollectionViewDataSou
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.selectedIndex = indexPath.row
collectionView.reloadData()
VPIAPManager.manager.start(model: dataArr[indexPath.row], shortPlayId: self.shortPlayId, videoId: self.videoId) { finish in
if finish {
VPLoginManager.manager.updateUserInfo(completer: nil)
}
}
}
}

View File

@ -9,6 +9,19 @@ import UIKit
class VPPlayerRechargeView: HWPanModalContentView {
var shortPlayId: String? {
didSet {
vipView.shortPlayId = shortPlayId
coinView.shortPlayId = shortPlayId
}
}
var videoId: String? {
didSet {
vipView.videoId = videoId
coinView.videoId = videoId
}
}
//MARK: UI
private lazy var bgView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "bg_image_01"))
@ -48,8 +61,15 @@ class VPPlayerRechargeView: HWPanModalContentView {
return view
}()
deinit {
NotificationCenter.default.removeObserver(self)
}
override init(frame: CGRect) {
super.init(frame: frame)
NotificationCenter.default.addObserver(self, selector: #selector(updateCoin), name: VPLoginManager.userInfoUpdateNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(buyVipFinishNotification), name: VPIAPManager.buyVipFinishNotification, object: nil)
vp_setupUI()
updateCoin()
@ -111,7 +131,7 @@ class VPPlayerRechargeView: HWPanModalContentView {
extension VPPlayerRechargeView {
private func updateCoin() {
@objc private func updateCoin() {
let coinCountStr = "\(VPLoginManager.manager.userInfo?.totalCoin ?? 0)"
let text = String(format: "Coins: %@".localized, coinCountStr)
let coinRange = text.ocString().range(of: coinCountStr)
@ -122,6 +142,13 @@ extension VPPlayerRechargeView {
coinLabel.attributedText = string
}
@objc private func buyVipFinishNotification() {
if VPLoginManager.manager.userInfo?.is_vip == true {
self.dismiss(animated: true) {
}
}
}
}
extension VPPlayerRechargeView {

View File

@ -15,6 +15,9 @@ class VPPlayerVipBuyView: UIView {
}
}
var shortPlayId: String?
var videoId: String?
private lazy var currentIndex: Int = 0
private lazy var collectionViewLayout: UICollectionViewFlowLayout = {
@ -140,5 +143,10 @@ extension VPPlayerVipBuyView: UICollectionViewDelegate, UICollectionViewDataSour
self.collectionView.reloadData()
}
VPIAPManager.manager.start(model: dataArr[indexPath.row], shortPlayId: self.shortPlayId, videoId: self.videoId) { finish in
if finish {
VPLoginManager.manager.updateUserInfo(completer: nil)
}
}
}
}

View File

@ -42,21 +42,24 @@ class VPVideoLockView: UIView {
}()
private lazy var coinCountLabel: UILabel = {
let userInfo = VPLoginManager.manager.userInfo
let label = UILabel()
label.font = .fontRegular(ofSize: 12)
label.textColor = .colorB5B5B5()
label.text = String(format: "Balance: %@ Coins | %@ Bonus".localized, "\(userInfo?.coin_left_total ?? 0)", "\(userInfo?.send_coin_left_total ?? 0)")
return label
}()
deinit {
NotificationCenter.default.removeObserver(self)
}
override init(frame: CGRect) {
super.init(frame: frame)
NotificationCenter.default.addObserver(self, selector: #selector(userInfoUpdateNotification), name: VPLoginManager.userInfoUpdateNotification, object: nil)
backgroundColor = .color000000(alpha: 0.8)
userInfoUpdateNotification()
vp_setupUI()
}
@ -68,6 +71,12 @@ class VPVideoLockView: UIView {
self.clickUnlockButton?()
}
@objc private func userInfoUpdateNotification() {
let userInfo = VPLoginManager.manager.userInfo
coinCountLabel.text = String(format: "Balance: %@ Coins | %@ Bonus".localized, "\(userInfo?.coin_left_total ?? 0)", "\(userInfo?.send_coin_left_total ?? 0)")
}
}
extension VPVideoLockView {

View File

@ -73,11 +73,16 @@ class VPCoinsViewController: VPViewController {
label.text = "kStoreTips".localized
return label
}()
deinit {
NotificationCenter.default.removeObserver(self)
}
override func viewDidLoad() {
super.viewDidLoad()
self.bgImageView.isHidden = true
self.view.backgroundColor = .clear
NotificationCenter.default.addObserver(self, selector: #selector(userInfoUpdateNotification), name: VPLoginManager.userInfoUpdateNotification, object: nil)
updateCoin()
vp_setupUI()
@ -112,6 +117,10 @@ class VPCoinsViewController: VPViewController {
self.collectionView.reloadData()
CATransaction.commit()
}
@objc private func userInfoUpdateNotification() {
updateCoin()
}
}
extension VPCoinsViewController {
@ -176,5 +185,14 @@ extension VPCoinsViewController: UICollectionViewDelegate, UICollectionViewDataS
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.selectedIndex = indexPath.row
collectionView.reloadData()
VPIAPManager.manager.start(model: dataArr[indexPath.row]) { finish in
if finish {
VPLoginManager.manager.updateUserInfo(completer: nil)
}
}
}
}

View File

@ -95,7 +95,9 @@ class VPVipPageViewController: VPViewController {
override func viewDidLoad() {
super.viewDidLoad()
let rightBar = UIBarButtonItem(title: "Restore".localized, style: .plain, target: self, action: #selector(handleRestore))
self.navigationItem.rightBarButtonItem = rightBar
vp_setupUI()
requestData()
@ -115,6 +117,14 @@ class VPVipPageViewController: VPViewController {
button.setBackgroundImage(UIImage(named: "vip_menu_right_selected"), for: .selected)
}
}
@objc private func handleRestore() {
VPIAPManager.manager.restore(isLoding: true) { isFinish in
if isFinish {
VPLoginManager.manager.updateUserInfo(completer: nil)
}
}
}
}
extension VPVipPageViewController {

View File

@ -219,5 +219,11 @@ extension VPVipViewController: UICollectionViewDelegate, UICollectionViewDataSou
self.collectionView.reloadData()
}
VPIAPManager.manager.start(model: dataArr[indexPath.row]) { finish in
if finish {
VPLoginManager.manager.updateUserInfo(completer: nil)
}
}
}
}

View File

@ -0,0 +1,193 @@
//
// VPIAPManager.swift
// Veloria
//
// Created by on 2025/6/5.
//
import UIKit
class VPIAPManager {
typealias CompletionHandler = ((_ finish: Bool) -> Void)
///
static let IAPPrefix = "veloria."
static let manager = VPIAPManager()
///
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: VPWaitRestoreModel? = UserDefaults.vp_object(forKey: kVPWaitRestoreIAPDefaultsKey, as: VPWaitRestoreModel.self)
///
func start(model: VPPayTemplateItem, shortPlayId: String? = nil, videoId: String? = nil, handler: CompletionHandler? = nil) {
if let _ = self.waitRestoreModel {
VPToast.show(text: "kIapErrorToast01".localized)
handler?(false)
return
}
guard let payId = model.id else {
handler?(false)
return
}
self.completionHandler = handler
self.waitRestoreModel = VPWaitRestoreModel()
self.waitRestoreModel?.buyType = model.buy_type
let productId = VPIAPManager.IAPPrefix + (model.ios_template_id ?? "")
VPHUD.show()
VPWalletAPI.requestCreateOrder(payId: payId, shortPlayId: shortPlayId ?? "0", videoId: videoId ?? "0") { orderModel in
guard let orderModel = orderModel else {
VPHUD.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 ?? "")
}
}
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 {
VPToast.show(text: "kIapErrorToast03".localized)
}
return
}
if isLoding {
VPHUD.show()
}
VPWalletAPI.requestVerifyOrder(orderCode: orderCode, payId: payId, productId: productId, purchaseToken: receipt) { model in
if isLoding {
VPHUD.dismiss()
}
guard let model = model else {
completer?(false)
return
}
let buyType = self.waitRestoreModel?.buyType
self.waitRestoreModel = nil
UserDefaults.vp_setObject(self.waitRestoreModel, forKey: kVPWaitRestoreIAPDefaultsKey)
if model.status == "success" {
if buyType == .subVip {
VPLoginManager.manager.userInfo?.is_vip = true
}
if isLoding {
VPToast.show(text: "Success".localized)
}
completer?(true)
if buyType == .subVip {
NotificationCenter.default.post(name: VPIAPManager.buyVipFinishNotification, object: nil)
}
} else {
completer?(false)
}
}
}
}
//MARK: -------------- JXIAPManagerDelegate --------------
extension VPIAPManager: 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.vp_setObject(self.waitRestoreModel, forKey: kVPWaitRestoreIAPDefaultsKey)
VPWalletAPI.requestVerifyOrder(orderCode: orderCode, payId: payId, productId: productId, purchaseToken: receipt) { model in
VPHUD.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.vp_setObject(self.waitRestoreModel, forKey: kVPWaitRestoreIAPDefaultsKey)
if model.status == "success" {
if buyType == .subVip {
VPLoginManager.manager.userInfo?.is_vip = true
}
VPToast.show(text: "Success".localized)
self.completionHandler?(true)
if buyType == .subVip {
NotificationCenter.default.post(name: VPIAPManager.buyVipFinishNotification, object: nil)
}
} else {
self.completionHandler?(false)
}
}
}
func jx_iapPayFailed(productId: String, code: JXIAPManagerCode) {
self.orderCode = nil
self.payId = nil
self.waitRestoreModel = nil
VPHUD.dismiss()
if code == .noProduct {
VPToast.show(text: "kIapErrorToast02".localized)
}
self.completionHandler?(false)
}
}
extension VPIAPManager {
///
@objc static let buyVipFinishNotification = NSNotification.Name(rawValue: "VPIAPManager.buyVipFinishNotification")
}

View File

@ -0,0 +1,19 @@
//
// VPIAPOrderModel.swift
// Veloria
//
// Created by on 2025/6/5.
//
import UIKit
import SmartCodable
class VPIAPOrderModel: VPModel, SmartCodable {
var code: Int?
var message: String?
var money: String?
var order_code: String?
var is_backhaul: String?
}

View File

@ -0,0 +1,17 @@
//
// VPIAPVerifyModel.swift
// Veloria
//
// Created by on 2025/6/5.
//
import UIKit
import SmartCodable
class VPIAPVerifyModel: VPModel, SmartCodable {
var code: String?
var status: String?
var money: String?
var is_backhaul: String?
}

View File

@ -0,0 +1,45 @@
//
// VPWaitRestoreModel.swift
// Veloria
//
// Created by on 2025/6/5.
//
import UIKit
class VPWaitRestoreModel: VPModel, NSSecureCoding {
var orderCode: String?
var payId: String?
var productId: String?
var receipt: String?
var buyType: VPWalletAPI.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 = VPWalletAPI.BuyType(rawValue: type)
}
}
}

View File

@ -84,6 +84,8 @@
"Check in" = "Check in";
"Expires in %@ days" = "Expires in 30 days";
"Expired" = "Expired";
"Success" = "Success";
"Restore" = "Restore";
"kHomeTitleText" = "10,000+ addictive shorts await!";
"kSearchPlaceholderText1" = "Search dramas";
@ -103,6 +105,13 @@
"kLockPreviousEpisodeText" = "The prequel to this series is not unlocked. Please unlock the prequel before unlocking this series";
//解锁失败
"kLockFailText" = "Purchase failed, please try again later!";
//已是会员
"kVipToast01" = "You are already a member!";
//还有未完成购买
"kIapErrorToast01" = "You have unfinished in-app purchases, please restore them first.";
"kIapErrorToast02" = "Invalid in-app purchase";
///没有可恢复购买
"kIapErrorToast03" = "There are no in-app purchases to restore.";

View File

@ -0,0 +1,207 @@
//
// JXIAPManager.swift
// BoJia
//
// Created by on 2024/6/3.
//
import UIKit
import StoreKit
@objc protocol JXIAPManagerDelegate {
///
@objc optional func jx_iapPayGotProducts(productIds: [String])
///
@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 orderId: String?
private var applicationUsername: String? {
get {
let id = "00000000-0000-0000-0000-000000000000"
guard let orderId = orderId else { return nil }
var string = ""
for i in 0..<orderId.length() {
if i == 12 || i == 16 || i == 20 || i == 24 {
string.insert("-", at: string.startIndex)
}
let s = orderId[orderId.index(orderId.endIndex, offsetBy: -(i + 1))]
string.insert(s, at: string.startIndex)
}
let length = id.length()
let stringLength = string.length()
if stringLength <= length {
let range = NSRange(location: length - string.length(), length: string.length())
return id.ocString().replacingCharacters(in: range, with: string)
} else {
return string
}
}
}
override init() {
super.init()
SKPaymentQueue.default().add(self)
}
func start(productId: String, orderId: String) {
self.product = nil
self.productId = productId
self.orderId = orderId
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
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) {
//
// if let _ = transaction.original, transaction.payment.applicationUsername == nil {
// return
// }
//
// if let _ = transaction.original, transaction.payment.applicationUsername != nil {
// self.delegate?.jx_iapPayFailed?(productId: productId, code: .unknown)
// return
// }
guard let productId = self.productId, productId == transaction.payment.productIdentifier else { return }
self.productId = nil
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 }
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)
}
}
}