Visual_Novel_iOS/crush/Crush/Src/Modules/VIP/View/VIPSubscribeSheet.swift

761 lines
26 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// VIPSubscribeSheet.swift
// Crush
//
// Created by Leon on 2025/9/16.
//
class VIPSubscribeSheet: EGPopBaseView {
var bgImageView: AutoRatioImageView!
var titleLabel: UILabel!
var closeButton: EPIconTertiaryButton!
var privilegesContainer: UIView!
var effectView: UIVisualEffectView!
var layout = UICollectionViewFlowLayout()
var cv: UICollectionView!
var leftButton: EPIconTertiaryDarkButton!
var rightButton: EPIconTertiaryDarkButton!
var agreenmentContainer: UIView!
var operateButton: StyleButton!
var tableView: UITableView!
// MARK: - Data Properties
private var vipProducts: [VIPSubcribeProduct] = []
private var privileges: [MemberPrivDict] = []
private var currentRealCvIndex = 0
/// 0~10
private var currentPrivilegeIndex = 0
private var selectedProductIndex = 0
private var multiplier = 100 //
private var initialScrollCompleted = false //
init() {
super.init(direction: .bottom)
contentView.backgroundColor = .c.csbn
contentLength = 693 + UIWindow.safeAreaBottom
setupViews()
setupData()
setupEvent()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Data
private func setupData(){
// Group load Hud
Hud.showIndicator()
let group = DispatchGroup()
group.enter()
CLPurchase.shared.loadVIPTiersProducts { [weak self] products in
group.leave()
// tableView
if let products = products {
self?.vipProducts = products
DispatchQueue.main.async {
self?.tableView.reloadData()
}
}
}
group.enter()
CLPurchase.shared.loadCurrentUserVipDetail { [weak self] output in
group.leave()
// get output.memberPrivList cv
if let output = output, let privileges = output.memberPrivList {
self?.privileges = privileges
DispatchQueue.main.async {
self?.cv.reloadData()
self?.updateNavigationButtons()
self?.initialScrollToMiddle() //
}
}
}
group.notify(queue: .main) {
// enter/leave
print("All completed")
Hud.hideIndicator()
}
}
private func setupEvent(){
NotificationCenter.default.addObserver(self, selector: #selector(notifyVIPChanged(_:)), name: AppNotificationName.vipStateChange.notificationName, object: nil)
}
@objc private func notifyVIPChanged(_ noti: Notification){
dismiss()
}
// MARK: - Functions
private func updateNavigationButtons() {
leftButton.isHidden = privileges.count <= 1
rightButton.isHidden = privileges.count <= 1
}
private func scrollToPrivilege(at index: Int) {
guard index >= 0 && index < privileges.count else { return }
currentPrivilegeIndex = index
//
let totalItems = privileges.count * multiplier
let middleStart = (totalItems / 2) - (totalItems / 2) % privileges.count
let targetIndex = middleStart + index
currentRealCvIndex = targetIndex
let indexPath = IndexPath(item: targetIndex, section: 0)
cv.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
private func initialScrollToMiddle() {
guard privileges.count > 0 && !initialScrollCompleted else { return }
let totalItems = privileges.count * multiplier
let middleStart = (totalItems / 2) - (totalItems / 2) % privileges.count
currentRealCvIndex = middleStart
let indexPath = IndexPath(item: middleStart, section: 0)
DispatchQueue.main.async {
self.cv.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false)
self.initialScrollCompleted = true
}
}
// MARK: - Action
@objc private func leftButtonPressed() {
guard privileges.count > 0 else { return }
//
let nextRealIndex = currentRealCvIndex - 1
//
let totalItems = privileges.count * multiplier
let middleStart = (totalItems / 2) - (totalItems / 2) % privileges.count
if nextRealIndex < 0 {
//
let targetIndex = totalItems - 1
currentRealCvIndex = targetIndex
currentPrivilegeIndex = targetIndex % privileges.count
let indexPath = IndexPath(item: targetIndex, section: 0)
cv.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
} else {
//
currentRealCvIndex = nextRealIndex
currentPrivilegeIndex = nextRealIndex % privileges.count
let indexPath = IndexPath(item: nextRealIndex, section: 0)
cv.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
}
@objc private func rightButtonPressed() {
guard privileges.count > 0 else { return }
//
let nextRealIndex = currentRealCvIndex + 1
//
let totalItems = privileges.count * multiplier
if nextRealIndex >= totalItems {
//
let targetIndex = 0
currentRealCvIndex = targetIndex
currentPrivilegeIndex = targetIndex % privileges.count
let indexPath = IndexPath(item: targetIndex, section: 0)
cv.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
} else {
//
currentRealCvIndex = nextRealIndex
currentPrivilegeIndex = nextRealIndex % privileges.count
let indexPath = IndexPath(item: nextRealIndex, section: 0)
cv.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
}
@objc private func termsOfServicePressed() {
AppRouter.goTermsOfService()
dismiss()
}
@objc private func privacyPolicyPressed() {
AppRouter.goPrivacyPolicy()
dismiss()
}
@objc private func tapBottomButton() {
// ...
guard selectedProductIndex < vipProducts.count else { return }
let selectedProduct = vipProducts[selectedProductIndex]
print("选择的产品: \(selectedProduct.productId ?? "")")
guard let productId = selectedProduct.productId, let payment = selectedProduct.payAmount else{
return
}
let product = IAPProducts()
product.productId = productId
product.chargeAmount = payment
IAPCore.shared.requestProducts([product]) {iapIds in
if iapIds.count > 0 {
let tradeId = String.randomNumber(length: 10)
IAPCore.shared.addPayProductId(productId: productId, tradeId: tradeId)
}
}
}
// MARK: - UI
private func setupViews() {
bgImageView = {
let v = AutoRatioImageView()
v.setImage(UIImage(named: "vip_sheet_bg"))
contentView.addSubview(v)
v.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
}
return v
}()
closeButton = {
let v = EPIconTertiaryButton(radius: .round, iconSize: .small, iconCode: .delete)
v.addTarget(self, action: #selector(bgButtonPressed), for: .touchUpInside)
contentView.addSubview(v)
v.snp.makeConstraints { make in
make.top.equalToSuperview().offset(20)
make.size.equalTo(v.bgImageSize())
make.trailing.equalToSuperview().offset(-16)
}
return v
}()
titleLabel = {
let v = UILabel()
v.font = .t.ttm
v.textColor = .text
v.textAlignment = .center
contentView.addSubview(v)
v.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(24)
make.trailing.equalToSuperview().offset(-24)
make.top.equalToSuperview().offset(24)
}
v.text = "Crushlevel VIP"
return v
}()
setupPrivilegeView()
setupAgreementsView()
operateButton = {
let v = StyleButton()
v.primary(size: .large)
contentView.addSubview(v)
v.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(CGFloat.lrs)
make.trailing.equalToSuperview().offset(-CGFloat.lrs)
make.bottom.equalTo(agreenmentContainer.snp.top).offset(-16)
}
v.addTarget(self, action: #selector(tapBottomButton), for: .touchUpInside)
v.setTitle("Subscribe", for: .normal)
return v
}()
setupVIPSubscribeItemsView()
}
private func setupPrivilegeView() {
privilegesContainer = {
let v = UIView()
v.cornerRadius = 12
contentView.addSubview(v)
v.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(CGFloat.lrs)
make.trailing.equalToSuperview().offset(-CGFloat.lrs)
make.top.equalToSuperview().offset(64)
make.height.equalTo(229)
}
return v
}()
effectView = {
let v = UIVisualEffectView(effect: UIBlurEffect(style: .light))
v.alpha = 0.9
privilegesContainer.addSubview(v)
v.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
return v
}()
// cv is in privilegesContainer, cellwidth and height is same as privilegesContainer. pageEnable = true, scroll hori..
cv = {
layout.scrollDirection = .horizontal
let width = UIScreen.width - 24 * 2
layout.itemSize = CGSize(width: width, height: 229)
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
let v = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)
v.backgroundColor = .clear
v.register(VIPPrivilegeGridCell.self, forCellWithReuseIdentifier: "VIPPrivilegeGridCell")
v.delegate = self
v.dataSource = self
v.isPagingEnabled = true
v.showsHorizontalScrollIndicator = false
privilegesContainer.addSubview(v)
v.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
return v
}()
leftButton = {
let v = EPIconTertiaryDarkButton(radius: .round, iconSize: .xs, iconCode: .arrowLeftBorder)
privilegesContainer.addSubview(v)
v.snp.makeConstraints { make in
make.top.equalToSuperview().offset(58.5)
make.leading.equalToSuperview().offset(16)
make.size.equalTo(v.bgImageSize())
}
return v
}()
rightButton = {
let v = EPIconTertiaryDarkButton(radius: .round, iconSize: .xs, iconCode: .arrowRightBorder)
privilegesContainer.addSubview(v)
v.snp.makeConstraints { make in
make.top.equalToSuperview().offset(58.5)
make.trailing.equalToSuperview().offset(-16)
make.size.equalTo(v.bgImageSize())
}
return v
}()
leftButton.addTarget(self, action: #selector(leftButtonPressed), for: .touchUpInside)
rightButton.addTarget(self, action: #selector(rightButtonPressed), for: .touchUpInside)
}
private func setupAgreementsView() {
agreenmentContainer = {
let v = UIView()
contentView.addSubview(v)
v.snp.makeConstraints { make in
make.height.equalTo(32)
make.leading.equalToSuperview()
make.trailing.equalToSuperview()
make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom)
}
return v
}()
let line = {
let v = CLLine()
agreenmentContainer.addSubview(v)
v.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(CGSize(width: 1, height: 8))
}
return v
}()
let leftButton = {
let v = UIButton()
v.titleLabel?.font = UIFont.t.tls
v.setTitleColor(.c.ctsn, for: .normal)
agreenmentContainer.addSubview(v)
v.snp.makeConstraints { make in
make.trailing.equalTo(line.snp.leading).offset(-16)
make.top.bottom.equalToSuperview()
}
v.addTarget(self, action: #selector(termsOfServicePressed), for: .touchUpInside)
return v
}()
let rightButton = {
let v = UIButton()
v.titleLabel?.font = UIFont.t.tls
v.setTitleColor(.c.ctsn, for: .normal)
agreenmentContainer.addSubview(v)
v.snp.makeConstraints { make in
make.leading.equalTo(line.snp.trailing).offset(16)
make.top.bottom.equalToSuperview()
}
v.addTarget(self, action: #selector(privacyPolicyPressed), for: .touchUpInside)
return v
}()
leftButton.setTitle("Terms of Service", for: .normal)
rightButton.setTitle("Privacy Policy", for: .normal)
}
private func setupVIPSubscribeItemsView() {
tableView = {
let v = UITableView()
v.backgroundColor = .clear
v.register(VIPTierListCell.self, forCellReuseIdentifier: "VIPTierListCell")
contentView.addSubview(v)
v.snp.makeConstraints { make in
make.leading.equalToSuperview()
make.trailing.equalToSuperview()
make.top.equalTo(privilegesContainer.snp.bottom).offset(16)
make.bottom.equalTo(operateButton.snp.top).offset(-16)
}
return v
}()
tableView.delegate = self
tableView.dataSource = self
}
}
extension VIPSubscribeSheet: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return privileges.count * multiplier
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "VIPPrivilegeGridCell", for: indexPath) as! VIPPrivilegeGridCell
let actualIndex = indexPath.item % privileges.count
if actualIndex < privileges.count {
let privilege = privileges[actualIndex]
cell.configure(with: privilege)
}
return cell
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
//
let pageWidth = scrollView.frame.width
let currentPage = Int(scrollView.contentOffset.x / pageWidth)
currentRealCvIndex = currentPage
currentPrivilegeIndex = currentPage % privileges.count
//
let totalItems = privileges.count * multiplier
let middleStart = (totalItems / 2) - (totalItems / 2) % privileges.count
if currentPage < privileges.count {
//
let targetIndex = middleStart + currentPrivilegeIndex
let targetIndexPath = IndexPath(item: targetIndex, section: 0)
DispatchQueue.main.async {
self.cv.scrollToItem(at: targetIndexPath, at: .centeredHorizontally, animated: false)
}
} else if currentPage >= totalItems - privileges.count {
//
let targetIndex = middleStart - (privileges.count - currentPrivilegeIndex)
let targetIndexPath = IndexPath(item: targetIndex, section: 0)
DispatchQueue.main.async {
self.cv.scrollToItem(at: targetIndexPath, at: .centeredHorizontally, animated: false)
}
}
}
}
extension VIPSubscribeSheet: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return vipProducts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "VIPTierListCell", for: indexPath) as! VIPTierListCell
if indexPath.row < vipProducts.count {
let product = vipProducts[indexPath.row]
cell.configure(with: product)
cell.setupSelected(selected: indexPath.row == selectedProductIndex)
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("didSelectRowAt \(indexPath)")
selectedProductIndex = indexPath.row
tableView.reloadData()
}
}
class VIPPrivilegeGridCell: UICollectionViewCell {
var icon: UIImageView!
var titleLabel: CLLabel!
var descLabel: LineSpaceLabel!
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
backgroundColor = .clear
icon = {
let v = UIImageView()
v.cornerRadius = 8
contentView.addSubview(v)
v.snp.makeConstraints { make in
make.size.equalTo(CGSize(width: 140, height: 93))
make.top.equalToSuperview().offset(24)
make.centerX.equalToSuperview()
}
return v
}()
titleLabel = {
let v = CLLabel()
v.font = .t.tts
v.textAlignment = .center
contentView.addSubview(v)
v.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16)
make.trailing.equalToSuperview().offset(-16)
make.top.equalTo(icon.snp.bottom).offset(16)
}
return v
}()
descLabel = {
let v = LineSpaceLabel()
let typo = CLSystemToken.typography(token: .tls)
v.textColor = .text
v.textAlignment = .center
v.config(typo)
contentView.addSubview(v)
v.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16)
make.trailing.equalToSuperview().offset(-16)
make.top.equalTo(titleLabel.snp.bottom).offset(8)
}
return v
}()
}
func configure(with privilege: MemberPrivDict) {
titleLabel.text = privilege.title ?? ""
descLabel.text = privilege.desc ?? ""
//
if let imgUrl = privilege.img, !imgUrl.isEmpty {
// 使
icon.loadImage(imgUrl, bgColor: .clear)
} else {
}
}
}
class VIPTierListCell: UITableViewCell {
var blockView: UIView!
var checkButton: EPRadioButton!
var coinAndLabel: CLIconLabel!
var periodLabel: CLLabel!
var descLabel: CLLabel!
var gradientFlag: GradientView!
var discountLabelOnFlag: CLLabel!
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
blockView = {
let v = UIView()
v.backgroundColor = .c.csnn
v.cornerRadius = 12
v.layer.borderWidth = 0
v.layer.borderColor = UIColor.c.cpn.cgColor
contentView.addSubview(v)
v.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(CGFloat.lrs)
make.trailing.equalToSuperview().offset(-CGFloat.lrs)
make.top.equalToSuperview()
make.bottom.equalToSuperview().offset(-12)
make.height.equalTo(80)
}
return v
}()
checkButton = {
let v = EPRadioButton()
v.setupRadioStyle(.checkEmpty)
v.isUserInteractionEnabled = false
blockView.addSubview(v)
v.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-24)
make.centerY.equalToSuperview()
}
return v
}()
coinAndLabel = {
let v = CLIconLabel()
v.iconSize = CGSize(width: 16, height: 16)
//v.iconImageView.image = UIImage(named: "icon_16_diamond")
v.iconImageView.isHidden = true
v.contentLabel.textColor = .text
v.contentLabel.font = .t.tnmm
blockView.addSubview(v)
v.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.trailing.equalTo(checkButton.snp.leading).offset(-8)
}
return v
}()
periodLabel = {
let v = CLLabel()
v.font = .t.tts
v.textColor = .text
blockView.addSubview(v)
v.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.leading.equalToSuperview().offset(24)
}
return v
}()
descLabel = {
let v = CLLabel()
v.font = .t.tls
v.textColor = .c.ctsn
blockView.addSubview(v)
v.snp.makeConstraints { make in
make.lastBaseline.equalTo(periodLabel)
make.leading.equalTo(periodLabel.snp.trailing).offset(4)
}
return v
}()
gradientFlag = {
let gradient = CLSystemToken.gradient(token: .ccvn)
let v = GradientView(colors: gradient.colors(), gradientType: .leftToRight)
v.cornerRadius = 12
v.backgroundColor = .c.csnn
v.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMaxYCorner]
blockView.addSubview(v)
v.snp.makeConstraints { make in
make.height.equalTo(24)
make.leading.top.equalToSuperview()
}
return v
}()
discountLabelOnFlag = {
let v = CLLabel()
v.font = .t.tbss
v.textColor = .c.cbd
gradientFlag.addSubview(v)
v.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.leading.equalToSuperview().offset(8)
make.trailing.equalToSuperview().offset(-8)
}
return v
}()
}
func configure(with product: VIPSubcribeProduct) {
//
// if let chargeAmount = product.chargeAmount {
// let coin = Coin(cents: chargeAmount)
// coinAndLabel.contentLabel.text = coin.formatted
// }
let period = product.period ?? .month
//
let periodText = getPeriodText(from: product.period)
periodLabel.text = periodText
//
if let payAmount = product.payAmount {
let price = Coin(cents: payAmount)
if period == .month{
descLabel.isHidden = true
}else if period == .season{
descLabel.isHidden = false
let pricePerMonth = CGFloat(payAmount) / 3.0
let pricePerMonthCoin = Coin(cents: Int(pricePerMonth))
descLabel.text = "$\(pricePerMonthCoin.formatted)/\(getPeriodShortText(from: product.period))"
}else if period == .year{
descLabel.isHidden = false
let pricePerMonth = CGFloat(payAmount) / 12.0
let pricePerMonthCoin = Coin(cents: Int(pricePerMonth))
descLabel.text = "$\(pricePerMonthCoin.formatted)/\(getPeriodShortText(from: product.period))"
}
// CrushCoin
coinAndLabel.contentLabel.text = "$\(price.formatted)"
}
//
if let discount = product.discount, let discountFloat = Float(discount), discountFloat > 0 {
gradientFlag.isHidden = false
discountLabelOnFlag.text = "\(discount)% Off"
} else {
gradientFlag.isHidden = true
}
}
private func getPeriodText(from period: PriceType?) -> String {
switch period {
case .month:
return "1 Month"
case .season:
return "3 Months"
case .year:
return "1 Year"
case .none:
return "1 Month"
}
}
private func getPeriodShortText(from period: PriceType?) -> String {
switch period {
case .month:
return "Month"
case .season:
return "Months"
case .year:
return "Year"
case .none:
return "Month"
}
}
func setupSelected(selected: Bool) {
checkButton.isSelected = selected
blockView.layer.borderWidth = selected ? 2 : 0
}
}