新增挽留弹窗

This commit is contained in:
澜声世纪 2026-02-02 17:47:55 +08:00
parent b3e52fac66
commit 5d55036f74
18 changed files with 818 additions and 11 deletions

View File

@ -91,6 +91,8 @@
85859E662EF3FC6E0020D282 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85859E632EF3FC6E0020D282 /* NotificationService.swift */; };
85859E682EFCD1D80020D282 /* NRHomeMustReadTodayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85859E672EFCD1D80020D282 /* NRHomeMustReadTodayTransformer.swift */; };
85ACA3F92F28A7CD009D52B0 /* NRNovelReadViewModel+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ACA3F82F28A7C8009D52B0 /* NRNovelReadViewModel+View.swift */; };
85B5B3982F30732800700E83 /* NRPayRetainAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B5B3972F30732800700E83 /* NRPayRetainAlert.swift */; };
85B5B39A2F30790600700E83 /* NRBorderLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B5B3992F30790600700E83 /* NRBorderLabel.swift */; };
85C1786B2F050AA400A8A76E /* Poppins-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 85C178682F050AA400A8A76E /* Poppins-Medium.ttf */; };
85C1786C2F050AA400A8A76E /* Poppins-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 85C1786A2F050AA400A8A76E /* Poppins-SemiBold.ttf */; };
85C1786D2F050AA400A8A76E /* Poppins-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 85C178672F050AA400A8A76E /* Poppins-Bold.ttf */; };
@ -602,6 +604,8 @@
85859E632EF3FC6E0020D282 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
85859E672EFCD1D80020D282 /* NRHomeMustReadTodayTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRHomeMustReadTodayTransformer.swift; sourceTree = "<group>"; };
85ACA3F82F28A7C8009D52B0 /* NRNovelReadViewModel+View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NRNovelReadViewModel+View.swift"; sourceTree = "<group>"; };
85B5B3972F30732800700E83 /* NRPayRetainAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRPayRetainAlert.swift; sourceTree = "<group>"; };
85B5B3992F30790600700E83 /* NRBorderLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRBorderLabel.swift; sourceTree = "<group>"; };
85C178672F050AA400A8A76E /* Poppins-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Bold.ttf"; sourceTree = "<group>"; };
85C178682F050AA400A8A76E /* Poppins-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Medium.ttf"; sourceTree = "<group>"; };
85C178692F050AA400A8A76E /* Poppins-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Regular.ttf"; sourceTree = "<group>"; };
@ -1080,6 +1084,7 @@
F3B859302EE66B950095A9CC /* NRDetailRechargeView.swift */,
85CF94282EED4664006467E3 /* NRVipRetainAlert.swift */,
85CF942A2EED47F2006467E3 /* NRVipRetainItemView.swift */,
85B5B3972F30732800700E83 /* NRPayRetainAlert.swift */,
);
path = V;
sourceTree = "<group>";
@ -1378,6 +1383,7 @@
0373D95F2ED59DA10017DCC7 /* NRSearchResultCell.swift */,
F34990BC2EDEC24E0039E939 /* NRStarGradeView.swift */,
85CF94262EED1734006467E3 /* NRHomeCoinsPackButton.swift */,
85B5B3992F30790600700E83 /* NRBorderLabel.swift */,
);
path = V;
sourceTree = "<group>";
@ -2804,6 +2810,7 @@
85CF95E02EF3CC93006467E3 /* QRLikeDataView.swift in Sources */,
85CF95E12EF3CC93006467E3 /* YLOBaitingView.swift in Sources */,
85CF95E22EF3CC93006467E3 /* UPPackCell.swift in Sources */,
85B5B39A2F30790600700E83 /* NRBorderLabel.swift in Sources */,
85CF95E32EF3CC93006467E3 /* NVDModalTextButton.swift in Sources */,
85CF95E42EF3CC93006467E3 /* NAStream.swift in Sources */,
85CF95E52EF3CC93006467E3 /* UWQCheckController.swift in Sources */,
@ -2991,6 +2998,7 @@
F3B8597D2EE9627B0095A9CC /* NRWalletHeaderView.swift in Sources */,
039810872ED057260006E317 /* NRUserDefaultsKey.swift in Sources */,
F34349222EDD227A00AA7E70 /* NRReadSettingSpacingView.swift in Sources */,
85B5B3982F30732800700E83 /* NRPayRetainAlert.swift in Sources */,
85CF94522EF14FBD006467E3 /* NROpenAppManager.swift in Sources */,
85606A902EEBA8F3005D989D /* NRCoinsPackConfirmView.swift in Sources */,
F34990F32EE02FD60039E939 /* NRNovelReadFinishViewController.swift in Sources */,
@ -3093,7 +3101,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.9;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.lssj.ReaderHive;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3138,7 +3146,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.9;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.lssj.ReaderHive;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -180,6 +180,26 @@ struct NRStoreAPI {
}
}
///
static func requestPayRetainInfo(completer: ((_ model: NRPayDateModel?) -> Void)?) {
var param = NRNetwork.Parameters(path: "/payRetrieveSettings")
param.method = .post
param.isLoding = false
param.isToast = false
param.parameters = [
"discount" : "1",
"purchases_token" : JXIAPManager.manager.getAppStoreReceipt() ?? "",
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRPayDateModel>) in
if (response.data?.list_coins?.count ?? 0) > 0 {
completer?(response.data)
} else {
completer?(nil)
}
}
}
///
static func requestCoinBagCanReceiveInfo() async -> NRCoinPackCanReceiveModel? {
await withCheckedContinuation { continuation in

View File

@ -0,0 +1,74 @@
//
// NRBorderLabel.swift
// ReaderHive
//
// Created by on 2026/2/2.
//
import UIKit
class NRBorderLabel: UILabel {
var borderLineWidth: CGFloat = 0
var borderImage: UIImage?
var borderColor: UIColor?
var borderColors: [CGColor]?
var borderStartPoint: CGPoint?
var borderEndPoint: CGPoint?
var textColorImage: UIImage?
var vp_textColor: UIColor?
var textColors: [CGColor]?
var textStartPoint: CGPoint?
var textEndPoint: CGPoint?
override var intrinsicContentSize: CGSize {
let size = super.intrinsicContentSize
return .init(width: size.width + borderLineWidth, height: size.height + borderLineWidth)
}
override func drawText(in rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext(),
let text = self.text,
let font = self.font else {
super.drawText(in: rect)
return
}
let attributes: [NSAttributedString.Key: Any] = [
.font: font
]
let textSize = text.size(withAttributes: attributes)
let x = borderLineWidth / 2
let y = borderLineWidth / 2
let textRect = CGRect(x: x, y: y, width: textSize.width, height: textSize.height)
let size = CGSizeMake(textSize.width + borderLineWidth / 2, textSize.height + borderLineWidth / 2)
context.setLineWidth(borderLineWidth)
context.setLineJoin(.round)
//
context.setTextDrawingMode(.stroke)
if let image = self.borderImage {
self.textColor = UIColor(patternImage: image.nr_resized(to: rect.size))
} else if let colors = self.borderColors, let startPoint = self.borderStartPoint, let endPoine = self.borderEndPoint {
self.textColor = UIColor(patternImage: UIImage.nr_getGradientImage(size: size, colors: colors, startPoint: startPoint, endPoint: endPoine))
} else {
self.textColor = self.borderColor ?? .clear
}
super.drawText(in: textRect)
//
context.setTextDrawingMode(.fill)
if let image = self.textColorImage {
self.textColor = UIColor(patternImage: image.nr_resized(to: rect.size))
} else if let colors = self.textColors, let startPoint = self.textStartPoint, let endPoine = self.textEndPoint {
self.textColor = UIColor(patternImage: UIImage.nr_getGradientImage(size: size, colors: colors, startPoint: startPoint, endPoint: endPoine))
} else {
self.textColor = vp_textColor //
}
super.drawText(in: textRect)
}
}

View File

@ -0,0 +1,541 @@
//
// NRPayRetainAlert.swift
// ReaderHive
//
// Created by on 2026/2/2.
//
import UIKit
import SnapKit
import YYCategories
import YYText
class NRPayRetainAlert: NRBaseAlert {
var buyFinishHandle: (() -> Void)?
var model: NRPayDateModel? {
didSet {
self.collectionView.reloadData()
self.titleLabel.text = model?.retrieve_lang?.title
self.countdownView.model = model
bonusTagView.update(text: model?.retrieve_lang?.subtitle ?? "")
}
}
var novelId: String?
var videoInfo: NRReadChapterCatalogModel?
private var selectedIndex = -1
private lazy var bgView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "pay_retain_bg_image"))
imageView.setContentHuggingPriority(.required, for: .horizontal)
imageView.setContentCompressionResistancePriority(.required, for: .horizontal)
imageView.isUserInteractionEnabled = true
return imageView
}()
private lazy var closeButton: UIButton = {
let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
self.dismiss()
}))
button.setImage(UIImage(named: "close_icon_01"), for: .normal)
return button
}()
private lazy var titleLabel: NRBorderLabel = {
let label = NRBorderLabel()
label.vp_textColor = .FFEFD_4
label.borderImage = UIImage(named: "gradient_color_01")
label.borderLineWidth = 12
label.font = .font(ofSize: 42, weight: .init(900)).withBoldItalic()
return label
}()
private lazy var countdownView: PayRetainCountdownView = {
let view = PayRetainCountdownView()
return view
}()
private lazy var bonusTagView: PayRetainBonusTagView = {
let view = PayRetainBonusTagView()
return view
}()
private lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.minimumLineSpacing = 7
layout.minimumInteritemSpacing = 0
layout.itemSize = .init(width: 300, height: 68)
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.backgroundColor = .clear
view.isScrollEnabled = true
view.showsVerticalScrollIndicator = false
view.dataSource = self
view.delegate = self
view.register(NRPayRetainPackCell.self, forCellWithReuseIdentifier: NRPayRetainPackCell.reuseIdentifier)
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
nr_setupUI()
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRPayRetainAlert {
private func nr_setupUI() {
contentView.addSubview(bgView)
contentView.addSubview(closeButton)
bgView.addSubview(titleLabel)
bgView.addSubview(countdownView)
bgView.addSubview(bonusTagView)
bgView.addSubview(collectionView)
bgView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
make.bottom.equalToSuperview().offset(-70)
}
closeButton.snp.makeConstraints { make in
make.centerX.equalToSuperview()
// make.top.equalTo(bgView.snp.bottom).offset(10)
make.bottom.equalToSuperview()
}
titleLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.right.lessThanOrEqualToSuperview()
make.top.equalToSuperview().offset(67)
}
countdownView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalTo(titleLabel.snp.bottom).offset(0)
}
bonusTagView.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(38)
make.centerX.equalToSuperview()
make.right.lessThanOrEqualToSuperview().offset(-10)
}
collectionView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(10)
make.right.equalToSuperview().offset(-10)
make.top.equalTo(bonusTagView.snp.bottom).offset(10)
make.bottom.equalToSuperview().offset(-10)
}
}
}
// MARK: - UICollectionViewDataSource & Delegate
extension NRPayRetainAlert: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.model?.list_coins?.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NRPayRetainPackCell.reuseIdentifier, for: indexPath) as! NRPayRetainPackCell
let item = self.model?.list_coins?[indexPath.row]
cell.configure(with: item, isSelected: indexPath.item == selectedIndex)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedIndex = indexPath.item
collectionView.reloadData()
guard let item = self.model?.list_coins?[indexPath.row] else { return }
NRIapManager.manager.start(model: item, shortPlayId: self.novelId, videoId: self.videoInfo?.id) { [weak self] finish in
guard let self = self else { return }
if finish {
Task {
await NRLoginManager.manager.updateUserInfo()
}
self.dismiss()
self.buyFinishHandle?()
}
}
}
}
extension NRPayRetainAlert {
final class PayRetainCountdownView: UIView {
var model: NRPayDateModel? {
didSet {
prefixLabel.text = model?.retrieve_lang?.remaining_time
suffixLabel.text = model?.retrieve_lang?.miss_out
}
}
#if DEBUG
private static let defaultCountdownSeconds = 10
#else
private static let defaultCountdownSeconds = 4 * 60 * 60
#endif
private static let countdownEndTimeKey = "NRPayRetainCountdownEndTime"
private var countdownTimer: Timer?
private var remainingSeconds: Int = 0
private lazy var resolveEndTime: TimeInterval = {
let defaults = UserDefaults.standard
let now = Date().timeIntervalSince1970
let storedEndTime = defaults.double(forKey: Self.countdownEndTimeKey)
if storedEndTime > now {
return storedEndTime
}
let newEndTime = now + TimeInterval(Self.defaultCountdownSeconds)
defaults.set(newEndTime, forKey: Self.countdownEndTimeKey)
return newEndTime
}()
private let prefixLabel = UILabel()
private let suffixLabel = UILabel()
private let timeStackView = UIStackView()
private let hourView = TimeBoxView()
private let minuteView = TimeBoxView()
private let secondView = TimeBoxView()
private let colonLabel1 = UILabel()
private let colonLabel2 = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
deinit {
stopCountdown()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMoveToWindow() {
super.didMoveToWindow()
if window != nil {
startCountdown()
} else {
stopCountdown()
}
}
private func setupView() {
let mainStack = UIStackView()
mainStack.axis = .horizontal
mainStack.alignment = .center
mainStack.spacing = 5
prefixLabel.font = .font(ofSize: 12, weight: .medium)
prefixLabel.textColor = UIColor._0_F_0_F_0_F
suffixLabel.font = .font(ofSize: 12, weight: .medium)
suffixLabel.textColor = UIColor._0_F_0_F_0_F
timeStackView.axis = .horizontal
timeStackView.alignment = .center
timeStackView.spacing = 3
colonLabel1.font = .font(ofSize: 12, weight: .medium)
colonLabel1.textColor = UIColor._0_F_0_F_0_F
colonLabel1.text = ":"
colonLabel2.font = .font(ofSize: 12, weight: .medium)
colonLabel2.textColor = UIColor._0_F_0_F_0_F
colonLabel2.text = ":"
timeStackView.addArrangedSubview(hourView)
timeStackView.addArrangedSubview(colonLabel1)
timeStackView.addArrangedSubview(minuteView)
timeStackView.addArrangedSubview(colonLabel2)
timeStackView.addArrangedSubview(secondView)
addSubview(mainStack)
mainStack.addArrangedSubview(prefixLabel)
mainStack.addArrangedSubview(timeStackView)
mainStack.addArrangedSubview(suffixLabel)
mainStack.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
// 4
private func startCountdown() {
stopCountdown()
let endTime = self.resolveEndTime
remainingSeconds = max(0, Int(endTime - Date().timeIntervalSince1970))
updateTimeLabels()
guard remainingSeconds > 0 else {
return
}
countdownTimer = Timer.scheduledTimer(timeInterval: 1,
target: YYTextWeakProxy(target: self),
selector: #selector(handleCountdownTimer),
userInfo: nil,
repeats: true)
}
private func stopCountdown() {
countdownTimer?.invalidate()
countdownTimer = nil
}
@objc private func handleCountdownTimer() {
guard remainingSeconds > 0 else {
updateTimeLabels()
stopCountdown()
return
}
remainingSeconds -= 1
updateTimeLabels()
}
private func updateTimeLabels() {
if remainingSeconds <= 0 {
self.isHidden = true
self.stopCountdown()
return
}
let hours = remainingSeconds / 3600
let minutes = (remainingSeconds % 3600) / 60
let seconds = remainingSeconds % 60
hourView.setText(String(format: "%02d", hours))
minuteView.setText(String(format: "%02d", minutes))
secondView.setText(String(format: "%02d", seconds))
}
private final class TimeBoxView: UIView {
private let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setText(_ text: String) {
label.text = text
}
private func setupView() {
backgroundColor = UIColor._0_F_0_F_0_F
layer.cornerRadius = 4
layer.masksToBounds = true
label.font = .font(ofSize: 12, weight: .medium)
label.textColor = .white
label.textAlignment = .center
addSubview(label)
label.snp.makeConstraints { make in
make.center.equalToSuperview()
}
snp.makeConstraints { make in
make.width.height.equalTo(18)
}
}
}
}
final class PayRetainBonusTagView: UIView {
private let leftStar = UIImageView(image: UIImage(named: "star_icon_06"))
private let rightStar = UIImageView(image: UIImage(named: "star_icon_06"))
private let titleLabel: NRLabel = {
let label = NRLabel()
label.font = .font(ofSize: 20, weight: .bold).withBoldItalic()
label.textColorImage = UIImage(named: "gradient_color_01")
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//
func update(text: String) {
titleLabel.text = text
}
private func setupView() {
addSubview(leftStar)
addSubview(rightStar)
addSubview(titleLabel)
leftStar.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().offset(0)
}
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview()
make.centerY.equalToSuperview()
make.left.equalTo(leftStar.snp.right).offset(5)
}
rightStar.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalTo(titleLabel.snp.right).offset(5)
make.right.equalToSuperview().offset(0)
}
}
}
}
// MARK: - Pack Cell
final class NRPayRetainPackCell: UICollectionViewCell {
static let reuseIdentifier = "cell"
private lazy var bgView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "pay_retain_cell_bg_image"))
return imageView
}()
private let selectedBorderView = UIView()
private let coinIconView = UIImageView(image: UIImage(named: "coins_icon_05"))
private let coinLabel = UILabel()
private let bonusLabel = UILabel()
private lazy var priceView: UIView = {
let view = NRGradientView()
view.colors = [UIColor.white.cgColor, UIColor.FDE_9_CB.cgColor]
view.startPoint = .init(x: 0, y: 0.5)
view.endPoint = .init(x: 1, y: 0.5)
view.layer.cornerRadius = 20
view.layer.masksToBounds = true
view.layer.borderWidth = 1
view.layer.borderColor = UIColor.black.withAlphaComponent(0.15).cgColor
return view
}()
private let priceLabel: UILabel = {
let label = UILabel()
label.textColor = ._946_A_37
label.font = .font(ofSize: 22, weight: .bold)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//
func configure(with item: NRPayItem?, isSelected: Bool) {
selectedBorderView.isHidden = !isSelected
guard let item = item else { return }
coinLabel.text = "\(item.coins ?? 0)"
bonusLabel.text = "+\((item.ext_info?.max_total_coins_pop ?? 0) - (item.ext_info?.max_total_coins ?? 0)) " + "free_coins".localized
var price = item.price
if let discountPrice = item.discount_price {
price = discountPrice
}
priceLabel.text = "\(item.currency ?? "")\(price ?? "")"
}
private func setupView() {
contentView.backgroundColor = .clear
// bgView.layer.cornerRadius = 15
// bgView.layer.masksToBounds = true
selectedBorderView.layer.cornerRadius = 12
selectedBorderView.layer.borderWidth = 2
selectedBorderView.layer.borderColor = UIColor.F_9710_D.cgColor
selectedBorderView.isHidden = true
coinIconView.contentMode = .scaleAspectFit
coinLabel.font = .font(ofSize: 22, weight: .bold)
coinLabel.textColor = ._946_A_37
bonusLabel.font = .font(ofSize: 12, weight: .medium)
bonusLabel.textColor = ._946_A_37
let coinStack = UIStackView(arrangedSubviews: [coinIconView, coinLabel])
coinStack.axis = .horizontal
coinStack.alignment = .center
coinStack.spacing = 2
contentView.addSubview(bgView)
contentView.addSubview(selectedBorderView)
bgView.addSubview(coinStack)
bgView.addSubview(bonusLabel)
bgView.addSubview(priceView)
priceView.addSubview(priceLabel)
bgView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
selectedBorderView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
coinStack.snp.makeConstraints { make in
make.left.equalToSuperview().offset(60)
make.top.equalToSuperview().offset(12)
}
coinIconView.snp.makeConstraints { make in
make.width.height.equalTo(22)
}
bonusLabel.snp.makeConstraints { make in
make.left.equalTo(coinStack)
make.top.equalTo(coinStack.snp.bottom).offset(2)
}
priceView.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-12)
make.centerY.equalToSuperview()
make.height.equalTo(40)
make.width.greaterThanOrEqualTo(90)
}
priceLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
make.right.lessThanOrEqualToSuperview().offset(-10)
}
}
}

View File

@ -158,10 +158,10 @@ extension NRNovelReadViewModel {
self.vc?.loadData()
}
// view.didDismissHandle = { [weak self] in
// guard let self = self else { return }
// self.showVipRetainAlert(catalogModel)
// }
view.didDismissHandle = { [weak self] in
guard let self = self else { return }
self.showPayRetainAlert(catalogModel)
}
view.present(in: nil)
self.popView = view
@ -190,4 +190,25 @@ extension NRNovelReadViewModel {
}
private func showPayRetainAlert(_ catalogModel: NRReadChapterCatalogModel) {
payDataRequest = NRPayDataRequest()
payDataRequest?.requestPayRetainInfo { [weak self] model in
guard let self = self else { return }
guard let model = model else { return }
let view = NRPayRetainAlert()
view.novelId = self.novelId
view.model = model
view.videoInfo = catalogModel
view.buyFinishHandle = { [weak self] in
guard let self = self else { return }
self.targetCatalogModel = catalogModel
self.vc?.loadData()
}
view.show(in: NRTool.keyWindow)
}
}
}

View File

@ -28,6 +28,18 @@ class NRPayDateModel: NSObject, SmartCodable {
///0: 1
var show_type: Int?
var retrieve_lang: NRPayRetrieveLang?
}
class NRPayRetrieveLang: NSObject, SmartCodable {
required override init() { }
var title: String?
var remaining_time: String?
var miss_out: String?
var subtitle: String?
var claim_reward: String?
}
class NRPayItem: NSObject, SmartCodable {
@ -95,6 +107,16 @@ class NRPayItem: NSObject, SmartCodable {
return product?.discounts
}
var discount_price: String? {
var price: String? = nil
if self.discount_type == 1, let introductoryPrice = self.introductionaryOffer {
price = introductoryPrice.price.stringValue
} else if self.discount_type == 2, let discount = self.promotionalOffers?.first {
price = discount.price.stringValue
}
return price
}
static func mappingForKey() -> [SmartKeyTransformer]? {
return [
@ -152,5 +174,6 @@ class NRPayExtInfo: NSObject, SmartCodable {
var max_total_coins: Int?
var extra_day_coins: Int?
var sub_coins_txt_list: [String]?
var max_total_coins_pop: Int?
}

View File

@ -15,6 +15,7 @@ class NRPayDataRequest: NSObject {
private var payAlertModel: NRPayAlertModel?
private var completerBlock: ((_ model: NRPayDateModel?) -> Void)?
private var payRetainBlock: ((_ model: NRPayDateModel?) -> Void)?
private var payAlertBlock: ((_ model: NRPayAlertModel?) -> Void)?
private var isLoding = false
@ -99,6 +100,33 @@ class NRPayDataRequest: NSObject {
productsRequest.start()
}
}
func requestPayRetainInfo(completer: ((_ model: NRPayDateModel?) -> Void)?) {
self.payRetainBlock = completer
self.isLoding = true
self.isToast = true
NRHud.show()
NRStoreAPI.requestPayRetainInfo { [weak self] model in
guard let self = self else { return }
guard let model = model else {
NRHud.dismiss()
self.payRetainBlock?(nil)
return
}
self.oldTemplateModel = model
var productIdArr: [String] = []
model.list_coins?.forEach { item in
productIdArr.append(NRIapManager.manager.getProductId(templateId: item.ios_template_id) ?? "")
}
let set = Set(productIdArr)
let productsRequest = SKProductsRequest(productIdentifiers: set)
productsRequest.delegate = self
productsRequest.start()
}
}
}
//MARK: SKProductsRequestDelegate
@ -110,7 +138,7 @@ extension NRPayDataRequest: SKProductsRequestDelegate {
}
let products = response.products
if let block = self.completerBlock {
if self.completerBlock != nil || self.payRetainBlock != nil {
guard let templateModel = self.oldTemplateModel else { return }
var newCoinList: [NRPayItem] = []
@ -147,11 +175,16 @@ extension NRPayDataRequest: SKProductsRequestDelegate {
templateModel.list_coins = newCoinList
templateModel.list_sub_vip = newVipList
if let block = self.completerBlock {
NRIapManager.manager.payDateModel = templateModel
DispatchQueue.main.async {
block(templateModel)
}
} else if let block = self.payRetainBlock {
DispatchQueue.main.async {
block(templateModel)
}
}
} else if let block = self.payAlertBlock {
guard let coinalertModel = self.payAlertModel else { return }

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x0F",
"green" : "0x0F",
"red" : "0x0F"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

View File

@ -152,6 +152,7 @@
"reader_chapter_price" = "Chapter Price";
"reader_my_vip_tips" = "Unrestricted access to all series!";
"reader_visitor" = "Visitor";
"free_coins" = "free coins";
"vip_retain_alert_text" = "Unlock every show you love!";
"reader_log_out_content" = "Are you sure you want to log out?";