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

761 lines
26 KiB
Swift
Raw Normal View History

2025-10-09 10:29:35 +00:00
//
// 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
}
}