123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493 |
- //
- // TSPurchaseVC.swift
- // TSLiveWallpaper
- //
- // Created by 100Years on 2025/1/14.
- //
- import Combine
- import SwiftUI
- import SwiftUIX
- class PurchaseViewModel : ObservableObject{
-
- @Published var selectedType: PremiumPeriod = .year
-
- /// 订阅publisher
- let buyPublisher = PassthroughSubject<Bool,Never>()
- /// 隐私
- let privacyPublisher = PassthroughSubject<Bool, Never>()
- /// term
- let termPublisher = PassthroughSubject<Bool, Never>()
- /// restore
- let restorePublisher = PassthroughSubject<Bool, Never>()
- }
- class TSPurchaseVC: TSBaseVC {
-
- var closePageBlock:(()->Void)?
-
- var viewModel: PurchaseViewModel = .init()
- var cancellabel: [AnyCancellable] = []
- var buyPeriod:PremiumPeriod = .year
- lazy var purchaseManager: PurchaseManager = {
- let purchaseManager = PurchaseManager.default
- return purchaseManager
- }()
-
-
- func createImageScroll(imageName:String,direction:ImagesAnimateScrollView.`Direction`)->ImagesAnimateScrollView{
- let imageScroll1: ImagesAnimateScrollView = ImagesAnimateScrollView()
- imageScroll1.direction = direction
- imageScroll1.animationImageName = imageName
- imageScroll1.transform = CGAffineTransform(rotationAngle: CGFloat.pi/12)
- return imageScroll1
- }
-
- lazy var bgView: UIView = {
- let bgView = UIView()
- bgView.backgroundColor = .clear
-
- let imageScroll1 = createImageScroll(imageName: "img-premium-photos-1", direction: .topToBottom)
- let imageScroll2 = createImageScroll(imageName: "img-premium-photos-2", direction: .bottomToTop)
- let imageScroll3 = createImageScroll(imageName: "img-premium-photos-3", direction: .topToBottom)
- let imageScroll4 = createImageScroll(imageName: "img-premium-photos-4", direction: .bottomToTop)
- let imageScroll5 = createImageScroll(imageName: "img-premium-photos-5", direction: .topToBottom)
-
- bgView.addSubview(imageScroll1)
- bgView.addSubview(imageScroll2)
- bgView.addSubview(imageScroll3)
- bgView.addSubview(imageScroll4)
- bgView.addSubview(imageScroll5)
-
- let top = -40.0
- let w = 110.0
- let h = 600//554.0
-
- //中间
- imageScroll3.snp.makeConstraints { make in
- make.top.equalTo(top)
- make.centerX.equalToSuperview()
- make.width.equalTo(w)
- make.height.equalTo(h)
- }
- //左边
- imageScroll2.snp.makeConstraints { make in
- make.top.equalTo(top)
- make.trailing.equalTo(imageScroll3.snp.leading).offset(-12)
- make.width.equalTo(w)
- make.height.equalTo(h)
- }
-
- imageScroll1.snp.makeConstraints { make in
- make.top.equalTo(top)
- make.trailing.equalTo(imageScroll2.snp.leading).offset(-12)
- make.width.equalTo(w)
- make.height.equalTo(h)
- }
-
- //右边
- imageScroll4.snp.makeConstraints { make in
- make.top.equalTo(top)
- make.leading.equalTo(imageScroll3.snp.trailing).offset(12)
- make.width.equalTo(w)
- make.height.equalTo(h)
- }
-
- imageScroll5.snp.makeConstraints { make in
- make.top.equalTo(top)
- make.leading.equalTo(imageScroll4.snp.trailing).offset(12)
- make.width.equalTo(w)
- make.height.equalTo(h)
- }
-
-
- kDelayMainShort {//0.1 秒后开启动画
- imageScroll1.startAnimation()
- imageScroll2.startAnimation()
- imageScroll3.startAnimation()
- imageScroll4.startAnimation()
- imageScroll5.startAnimation()
- }
-
- let imageView = UIImageView.createImageView(imageName: "purchase_bj",contentMode: .scaleAspectFill)
- bgView.addSubview(imageView)
-
- imageView.snp.makeConstraints { make in
- make.edges.equalToSuperview()
- }
-
- return bgView
- }()
-
- lazy var hostVc: UIHostingController<PurchaseView> = {
- let vc = UIHostingController(rootView: PurchaseView(viewModel: viewModel))
- vc.view.backgroundColor = .clear
- return vc
- }()
-
- override func createView() {
- addNormalNavBarView()
- _ = setNavigationItem("", imageName: "close_gray", direction: .left, action: #selector(closePage))
-
- view.insertSubview(bgView, at: 0)
- bgView.snp.makeConstraints { make in
- make.leading.trailing.bottom.top.equalToSuperview()
- }
-
- // setViewBgImageNamed(named: "purchase_bj")
- contentView.addSubview(hostVc.view)
- hostVc.view.snp.makeConstraints { make in
- make.leading.trailing.bottom.top.equalToSuperview()
- }
- }
-
- override func dealThings() {
- addNotifaction()
- onPurchaseStateChanged()
- }
-
-
- func addNotifaction() {
- viewModel.buyPublisher.receive(on: DispatchQueue.main).sink { [weak self] _ in
- guard let self = self else {
- return
- }
- PurchaseManager.default.pay(for: self.viewModel.selectedType)
- }.store(in: &cancellabel)
- viewModel.privacyPublisher.receive(on: DispatchQueue.main).sink { [weak self] _ in
- guard let self = self else {
- return
- }
-
- let vc = TSBusinessWebVC(urlType: .privacy)
- vc.hidesBottomBarWhenPushed = true
- kPresentModalVC(target: self, modelVC: vc)
-
- }.store(in: &cancellabel)
- viewModel.termPublisher.receive(on: DispatchQueue.main).sink { [weak self] _ in
- guard let self = self else {
- return
- }
-
- let vc = TSBusinessWebVC(urlType: .terms)
- vc.hidesBottomBarWhenPushed = true
- kPresentModalVC(target: self, modelVC: vc)
- }.store(in: &cancellabel)
- viewModel.restorePublisher.receive(on: DispatchQueue.main).sink { _ in
- PurchaseManager.default.restorePremium()
- }.store(in: &cancellabel)
- }
-
-
- func onPurchaseStateChanged(){
- purchaseManager.onPurchaseStateChanged = { [weak self] manager,state,object in
- guard let self = self else { return }
-
- DispatchQueue.main.async {
- switch state {
- case .none:
- break
- case .loading:
- TSToastShared.showLoading(text: "Getting price".localized,containerView: self.view)
- case .loadSuccess:
- TSToastShared.hideLoading()
- case .loadFail:
- TSToastShared.hideLoading()
- let message = "Failed to get the price, will automatically retry in 5 seconds".localized
- TSToastShared.showToast(text: message)
- DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
- PurchaseManager.default.requestProducts()
- }
- case .paying:
- TSToastShared.showLoading(text: "Purchasing now".localized,containerView: self.view)
- case .paySuccess:
- TSToastShared.hideLoading()
- let loadingText = manager.isVip ? "Congratulation you have become VIP".localized : "Finish".localized
- TSToastShared.showToast(text:loadingText)
- if manager.isVip {
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
- self.closePage()
- }
- }
- case .payFail:
- TSToastShared.hideLoading()
- if let str = object as? String {
- TSToastShared.showToast(text: str)
- }
-
- case .restoreing:
- TSToastShared.showLoading(text: "Restoring now".localized,containerView: self.view)
- case .restoreSuccess:
- TSToastShared.hideLoading()
- let loadingText = manager.isVip ? "Congratulation you have become VIP".localized : "Couldn't Restore Subscription".localized
- debugPrint(loadingText)
- TSToastShared.showToast(text:loadingText)
- if manager.isVip {
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
- self.closePage()
- }
- }
- case .restoreFail:
- TSToastShared.hideLoading()
- let loadingText = (object as? String) ?? "Failed to restore subscribe, please try again".localized
- debugPrint(loadingText)
- TSToastShared.showToast(text: loadingText)
- case .verifying:
- #if DEBUG
- TSToastShared.showLoading(text: "Verifying receipt...".localized,containerView: self.view)
- #endif
- case .verifySuccess:
- break
- case .verifyFail:
- #if DEBUG
- TSToastShared.hideLoading()
- let message = (object as? String) ?? "Failed to validate receipt".localized
- TSToastShared.showToast(text:message)
- #endif
- }
- }
- debugPrint("PurchaseManager onPurchaseStateChanged=\(String(describing: state))")
- }
- }
-
- @objc func closePage(){
- closePageBlock?()
- TSToastShared.hideLoading()
- self.dismiss(animated: true)
- }
-
-
- deinit {
- cancellabel.removeAll()
- }
- }
- func kJudgeVip(externalBool:Bool,
- vc:UIViewController,
- closePageBlock:(()->Void)?) -> Bool {
- //判断 vip
- if externalBool,
- PurchaseManager.default.isVip == false
- {
- TSPurchaseVC.show(target: vc, closePageBlock: nil)
- return true
- }
- return false
- }
- extension TSPurchaseVC{
-
- static func show(target:UIViewController,closePageBlock:(()->Void)?){
- let vc = TSPurchaseVC()
- vc.closePageBlock = closePageBlock
- let navi = TSBaseNavigationC(rootViewController: vc)
- navi.modalPresentationStyle = .overFullScreen
- target.present(navi, animated: true)
- }
- }
- struct PurchaseView :View {
-
- @ObservedObject var viewModel: PurchaseViewModel
-
- var body: some View {
- ScrollView {
- Spacer().frame(height: 31)
-
- VStack {
-
- customText(text: "\(kAppName) ",fontName: .KelsiFill,color:UIColor.themeColor.color).frame(height: 53*kDesignScale)
-
- Spacer().frame(height: 16)
-
- Text("Premium".localized)
- .font(.font(size: 16))
- .padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20))
- .frame(height: 34)
- .foregroundColor(.white)
- .background(.hex("#222222"))
- .cornerRadius(17.0)
- .overlay(
- RoundedRectangle(cornerRadius: 16)
- .stroke(Color.hex("#FA794F"), lineWidth: 2) // 边框
- )
- Spacer().frame(height: 36)
-
- ZStack {
- VStack(alignment: .leading,spacing: 16) {
-
- HStack(spacing: 12) {
- Image("vip_feature_1").resizable().frame(width: 24, height: 24)
- Text("Unlimited AI image generation".localized)
- }
-
- HStack(spacing: 12) {
- Image("vip_feature_2").resizable().frame(width: 24, height: 24)
- Text("Change image styles".localized)
- }
-
- HStack(spacing: 12) {
- Image("vip_feature_3").resizable().frame(width: 24, height: 24)
- Text("Unlimited AI chat".localized).multilineTextAlignment(.leading)
- }
-
- HStack(spacing: 12) {
- Image("vip_feature_4").resizable().frame(width: 24, height: 24)
- Text("Ad-free experience".localized).multilineTextAlignment(.leading)
- }
-
- }.font(.font(size: 16)).foregroundColor(UIColor.white.color)
- }
-
- }
-
- Spacer().frame(height: 44)
-
-
- VStack(spacing: 12) {
-
- ZStack(alignment: .topTrailing) {
- PurchaseItemView(title: "One Year".localized, type: .year, selectedType: $viewModel.selectedType).onTapGesture {
- viewModel.selectedType = .year
- }
- TSVipRecView()
- .offset(y:-14)
- }
-
- HStack {
- PurchaseItemView(title: "One Month".localized, type: .month, selectedType: $viewModel.selectedType).onTapGesture {
- viewModel.selectedType = .month
- }
- PurchaseItemView(title: "One Week".localized, type: .week, selectedType: $viewModel.selectedType).onTapGesture {
- viewModel.selectedType = .week
- }
- }
- Button {
- viewModel.buyPublisher.send(true)
- } label: {
- ZStack {
- Color.hex("#FFBD59")
- Text("Continue")
- .font(.system(size: 16))
- .foregroundColor(.hex("#111111"))
-
- }.frame(maxWidth: .infinity ,minHeight: 48.0)
- .cornerRadius(24.0)
- }
-
- HStack {
- Text("Recurring billing, cancel anytime".localized)
- .foregroundColor(Color.hex("#FFBD59")) +
- Text(",Payment will be charged to your iTunes account at confirmation of purchase. Subscriptions automatically renew for the same applicable term and price, unless auto-renew is turned off at least 24 hours before the end of the current period.".localized)
- .foregroundColor(UIColor.lesserText.color)
- }
- .multilineTextAlignment(.center).font(.font(size: 8))
- .onTapGesture {
- viewModel.privacyPublisher.send(true)
- }
-
- Spacer().frame(height: 6.0)
- HStack(spacing: 8) {
- Text("Terms of us".localized)
- .onTapGesture {
- viewModel.termPublisher.send(true)
- }
- Text("|")
- Text("Privacy Policy".localized)
- .onTapGesture {
- viewModel.privacyPublisher.send(true)
- }
- Text("|")
- Text("Restore".localized)
- .onTapGesture {
- viewModel.restorePublisher.send(true)
- }
- }.font(.system(size: 12)).foregroundColor(.hex("#999999"))
- }.padding(.horizontal)
- }
- }
-
- // 定义一个返回 View 的方法
- func customText(text:String,fontName:FontName,color:Color) -> some View {
- let gorgeousColor = color //UIColor.themeColor.color
- return Text(text)
- .font(.font(name: fontName,size: 48))
-
- .gradientForeground(
- colors: [.hex("#FA794F"),.hex("#F8C32A"),.hex("#FEFBF4")],
- startPoint: UnitPoint.leading,
- endPoint: UnitPoint.trailing
- )
-
-
- // .shadow(color: gorgeousColor.opacity(0.7), radius: 6, x: 0, y: 0)
-
- .foregroundColor(gorgeousColor)
- .frame(height: 20)
- }
- }
- struct PurchaseItemView: View {
- var title: String
- var type: PremiumPeriod
- @Binding var selectedType: PremiumPeriod
- var body: some View {
- ZStack {
- Color.white.opacity(0.1)
- HStack {
- VStack(alignment: .leading, spacing: 8) {
- Text(title).font(.font(size: 14)).foregroundColor(UIColor.textAssist.color)
- Text(PurchaseManager.default.price(for: type) ?? "--").font(.font(size: 18,weight: .medium)).foregroundColor(UIColor.mainText.color)
- }
- Spacer()
- if type == selectedType {
- Image(.radioboxSelected)
- }
- }.padding(.horizontal)
- }
- .frame(height: 74) // 设置高度
- .cornerRadius(16.0) // 圆角
- .overlay(
- RoundedRectangle(cornerRadius: 16)
- .stroke(Color.hex("#FECB34"), lineWidth: type == selectedType ? 1 : 0) // 边框
- )
- }
- }
- //推荐选择view
- struct TSVipRecView: View {
- var body: some View {
-
- HStack(spacing: 4) {
- Image("upvote_black").resizable().frame(width: 16, height: 16)
- Text("Save-Vip".localized + "60%").font(.font(size: 12,weight: .medium)).foregroundColor(.hex("#111111"))
- }
- .padding(EdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6))
- .background(Color.hex("#FECB34"))
- .frame(height: 28) // 设置高度
- .cornerRadius([.topLeading, .topTrailing, .bottomLeading], 16.0)
- }
- }
|