diff --git a/ReaderHive.xcodeproj/project.pbxproj b/ReaderHive.xcodeproj/project.pbxproj index 52a0ee8..e499dec 100644 --- a/ReaderHive.xcodeproj/project.pbxproj +++ b/ReaderHive.xcodeproj/project.pbxproj @@ -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 = ""; }; 85859E672EFCD1D80020D282 /* NRHomeMustReadTodayTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRHomeMustReadTodayTransformer.swift; sourceTree = ""; }; 85ACA3F82F28A7C8009D52B0 /* NRNovelReadViewModel+View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NRNovelReadViewModel+View.swift"; sourceTree = ""; }; + 85B5B3972F30732800700E83 /* NRPayRetainAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRPayRetainAlert.swift; sourceTree = ""; }; + 85B5B3992F30790600700E83 /* NRBorderLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRBorderLabel.swift; sourceTree = ""; }; 85C178672F050AA400A8A76E /* Poppins-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Bold.ttf"; sourceTree = ""; }; 85C178682F050AA400A8A76E /* Poppins-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Medium.ttf"; sourceTree = ""; }; 85C178692F050AA400A8A76E /* Poppins-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Regular.ttf"; sourceTree = ""; }; @@ -1080,6 +1084,7 @@ F3B859302EE66B950095A9CC /* NRDetailRechargeView.swift */, 85CF94282EED4664006467E3 /* NRVipRetainAlert.swift */, 85CF942A2EED47F2006467E3 /* NRVipRetainItemView.swift */, + 85B5B3972F30732800700E83 /* NRPayRetainAlert.swift */, ); path = V; sourceTree = ""; @@ -1378,6 +1383,7 @@ 0373D95F2ED59DA10017DCC7 /* NRSearchResultCell.swift */, F34990BC2EDEC24E0039E939 /* NRStarGradeView.swift */, 85CF94262EED1734006467E3 /* NRHomeCoinsPackButton.swift */, + 85B5B3992F30790600700E83 /* NRBorderLabel.swift */, ); path = V; sourceTree = ""; @@ -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 = ""; diff --git a/ReaderHive/Base/Networking/API/NRStoreAPI.swift b/ReaderHive/Base/Networking/API/NRStoreAPI.swift index 9c2a62c..c7defcc 100644 --- a/ReaderHive/Base/Networking/API/NRStoreAPI.swift +++ b/ReaderHive/Base/Networking/API/NRStoreAPI.swift @@ -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) in + if (response.data?.list_coins?.count ?? 0) > 0 { + completer?(response.data) + } else { + completer?(nil) + } + } + } + ///获取金币包可领取信息 static func requestCoinBagCanReceiveInfo() async -> NRCoinPackCanReceiveModel? { await withCheckedContinuation { continuation in diff --git a/ReaderHive/Class/Home/V/NRBorderLabel.swift b/ReaderHive/Class/Home/V/NRBorderLabel.swift new file mode 100644 index 0000000..a9b6056 --- /dev/null +++ b/ReaderHive/Class/Home/V/NRBorderLabel.swift @@ -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) + } + +} diff --git a/ReaderHive/Class/Novel/V/NRPayRetainAlert.swift b/ReaderHive/Class/Novel/V/NRPayRetainAlert.swift new file mode 100644 index 0000000..2bb32ae --- /dev/null +++ b/ReaderHive/Class/Novel/V/NRPayRetainAlert.swift @@ -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) + } + } + +} diff --git a/ReaderHive/Class/Novel/VM/NRNovelReadViewModel+View.swift b/ReaderHive/Class/Novel/VM/NRNovelReadViewModel+View.swift index e3a8394..08f5f76 100644 --- a/ReaderHive/Class/Novel/VM/NRNovelReadViewModel+View.swift +++ b/ReaderHive/Class/Novel/VM/NRNovelReadViewModel+View.swift @@ -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) + } + } + } diff --git a/ReaderHive/Class/Store/M/NRPayDateModel.swift b/ReaderHive/Class/Store/M/NRPayDateModel.swift index 374dfaa..539ef75 100644 --- a/ReaderHive/Class/Store/M/NRPayDateModel.swift +++ b/ReaderHive/Class/Store/M/NRPayDateModel.swift @@ -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? } diff --git a/ReaderHive/Libs/IAP/NRPayDataRequest.swift b/ReaderHive/Libs/IAP/NRPayDataRequest.swift index 5b0ef10..840f85e 100644 --- a/ReaderHive/Libs/IAP/NRPayDataRequest.swift +++ b/ReaderHive/Libs/IAP/NRPayDataRequest.swift @@ -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,10 +175,15 @@ extension NRPayDataRequest: SKProductsRequestDelegate { templateModel.list_coins = newCoinList templateModel.list_sub_vip = newVipList - NRIapManager.manager.payDateModel = templateModel - - DispatchQueue.main.async { - block(templateModel) + 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 { diff --git a/ReaderHive/Source/Assets.xcassets/Color/#0F0F0F.colorset/Contents.json b/ReaderHive/Source/Assets.xcassets/Color/#0F0F0F.colorset/Contents.json new file mode 100644 index 0000000..ad2a076 --- /dev/null +++ b/ReaderHive/Source/Assets.xcassets/Color/#0F0F0F.colorset/Contents.json @@ -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 + } +} diff --git a/ReaderHive/Source/Assets.xcassets/Image/pay_retain_bg_image.imageset/Contents.json b/ReaderHive/Source/Assets.xcassets/Image/pay_retain_bg_image.imageset/Contents.json new file mode 100644 index 0000000..36577cc --- /dev/null +++ b/ReaderHive/Source/Assets.xcassets/Image/pay_retain_bg_image.imageset/Contents.json @@ -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 + } +} diff --git a/ReaderHive/Source/Assets.xcassets/Image/pay_retain_bg_image.imageset/bg@2x.png b/ReaderHive/Source/Assets.xcassets/Image/pay_retain_bg_image.imageset/bg@2x.png new file mode 100644 index 0000000..88f2265 Binary files /dev/null and b/ReaderHive/Source/Assets.xcassets/Image/pay_retain_bg_image.imageset/bg@2x.png differ diff --git a/ReaderHive/Source/Assets.xcassets/Image/pay_retain_bg_image.imageset/bg@3x.png b/ReaderHive/Source/Assets.xcassets/Image/pay_retain_bg_image.imageset/bg@3x.png new file mode 100644 index 0000000..35de10d Binary files /dev/null and b/ReaderHive/Source/Assets.xcassets/Image/pay_retain_bg_image.imageset/bg@3x.png differ diff --git a/ReaderHive/Source/Assets.xcassets/Image/pay_retain_cell_bg_image.imageset/Contents.json b/ReaderHive/Source/Assets.xcassets/Image/pay_retain_cell_bg_image.imageset/Contents.json new file mode 100644 index 0000000..1435009 --- /dev/null +++ b/ReaderHive/Source/Assets.xcassets/Image/pay_retain_cell_bg_image.imageset/Contents.json @@ -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 + } +} diff --git a/ReaderHive/Source/Assets.xcassets/Image/pay_retain_cell_bg_image.imageset/金币包背景@2x.png b/ReaderHive/Source/Assets.xcassets/Image/pay_retain_cell_bg_image.imageset/金币包背景@2x.png new file mode 100644 index 0000000..350be34 Binary files /dev/null and b/ReaderHive/Source/Assets.xcassets/Image/pay_retain_cell_bg_image.imageset/金币包背景@2x.png differ diff --git a/ReaderHive/Source/Assets.xcassets/Image/pay_retain_cell_bg_image.imageset/金币包背景@3x.png b/ReaderHive/Source/Assets.xcassets/Image/pay_retain_cell_bg_image.imageset/金币包背景@3x.png new file mode 100644 index 0000000..d3e9b59 Binary files /dev/null and b/ReaderHive/Source/Assets.xcassets/Image/pay_retain_cell_bg_image.imageset/金币包背景@3x.png differ diff --git a/ReaderHive/Source/Assets.xcassets/Image/star_icon_06.imageset/Contents.json b/ReaderHive/Source/Assets.xcassets/Image/star_icon_06.imageset/Contents.json new file mode 100644 index 0000000..bb75433 --- /dev/null +++ b/ReaderHive/Source/Assets.xcassets/Image/star_icon_06.imageset/Contents.json @@ -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 + } +} diff --git a/ReaderHive/Source/Assets.xcassets/Image/star_icon_06.imageset/star@2x.png b/ReaderHive/Source/Assets.xcassets/Image/star_icon_06.imageset/star@2x.png new file mode 100644 index 0000000..fd36e84 Binary files /dev/null and b/ReaderHive/Source/Assets.xcassets/Image/star_icon_06.imageset/star@2x.png differ diff --git a/ReaderHive/Source/Assets.xcassets/Image/star_icon_06.imageset/star@3x.png b/ReaderHive/Source/Assets.xcassets/Image/star_icon_06.imageset/star@3x.png new file mode 100644 index 0000000..98b3603 Binary files /dev/null and b/ReaderHive/Source/Assets.xcassets/Image/star_icon_06.imageset/star@3x.png differ diff --git a/ReaderHive/Source/en.lproj/Localizable.strings b/ReaderHive/Source/en.lproj/Localizable.strings index 574fa95..c6572bd 100644 --- a/ReaderHive/Source/en.lproj/Localizable.strings +++ b/ReaderHive/Source/en.lproj/Localizable.strings @@ -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?";