TSPurchaseVC.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. //
  2. // TSPurchaseVC.swift
  3. // TSLiveWallpaper
  4. //
  5. // Created by 100Years on 2025/1/14.
  6. //
  7. import Combine
  8. import SwiftUI
  9. import SwiftUIX
  10. class PurchaseViewModel : ObservableObject{
  11. @Published var selectedType: PremiumPeriod = .year
  12. /// 订阅publisher
  13. let buyPublisher = PassthroughSubject<Bool,Never>()
  14. /// 隐私
  15. let privacyPublisher = PassthroughSubject<Bool, Never>()
  16. /// term
  17. let termPublisher = PassthroughSubject<Bool, Never>()
  18. /// restore
  19. let restorePublisher = PassthroughSubject<Bool, Never>()
  20. }
  21. class TSPurchaseVC: TSBaseVC {
  22. var closePageBlock:(()->Void)?
  23. var viewModel: PurchaseViewModel = .init()
  24. var cancellabel: [AnyCancellable] = []
  25. var buyPeriod:PremiumPeriod = .year
  26. lazy var purchaseManager: PurchaseManager = {
  27. let purchaseManager = PurchaseManager.default
  28. return purchaseManager
  29. }()
  30. private var isHandlePurchaseStateChanged = true //是否处理购买状态变化
  31. func createImageScroll(imageName:String,direction:ImagesAnimateScrollView.`Direction`)->ImagesAnimateScrollView{
  32. let imageScroll1: ImagesAnimateScrollView = ImagesAnimateScrollView()
  33. imageScroll1.direction = direction
  34. imageScroll1.animationImageName = imageName
  35. imageScroll1.transform = CGAffineTransform(rotationAngle: CGFloat.pi/12)
  36. return imageScroll1
  37. }
  38. lazy var imageComparisonView: TYCycleImageComparisonView = {
  39. let imageComparisonView = TYCycleImageComparisonView()
  40. imageComparisonView.frame = CGRect(x: 0, y: 0, width: k_ScreenWidth, height: 532*kDesignScale)
  41. imageComparisonView.itemModelArray = [
  42. TSImageComparisonModel(oldImage: UIImage(named: "image_comparison_old_0")!, newImage: UIImage(named: "image_comparison_new_0")!),
  43. TSImageComparisonModel(oldImage: UIImage(named: "image_comparison_old_1")!, newImage: UIImage(named: "image_comparison_new_1")!),
  44. TSImageComparisonModel(oldImage: UIImage(named: "image_comparison_old_2")!, newImage: UIImage(named: "image_comparison_new_2")!),
  45. TSImageComparisonModel(oldImage: UIImage(named: "image_comparison_old_3")!, newImage: UIImage(named: "image_comparison_new_3")!),
  46. TSImageComparisonModel(oldImage: UIImage(named: "image_comparison_old_4")!, newImage: UIImage(named: "image_comparison_new_4")!)
  47. ]
  48. return imageComparisonView
  49. }()
  50. lazy var bgView: UIView = {
  51. let bgView = UIView()
  52. bgView.backgroundColor = .clear
  53. let imageView = UIImageView.createImageView(imageName: "purchase_bj",contentMode: .scaleAspectFill)
  54. bgView.addSubview(imageView)
  55. imageView.snp.makeConstraints { make in
  56. make.edges.equalToSuperview()
  57. }
  58. return bgView
  59. }()
  60. lazy var hostVc: UIHostingController<PurchaseView> = {
  61. if PurchaseManager.default.vipType == .none {
  62. viewModel.selectedType = .month
  63. }
  64. let vc = UIHostingController(rootView: PurchaseView(viewModel: viewModel))
  65. vc.view.backgroundColor = .clear
  66. return vc
  67. }()
  68. override func createView() {
  69. addNormalNavBarView()
  70. _ = setNavigationItem("", imageName: "close_gray", direction: .left, action: #selector(closePage))
  71. view.insertSubview(bgView, at: 0)
  72. bgView.snp.makeConstraints { make in
  73. make.leading.trailing.bottom.top.equalToSuperview()
  74. }
  75. bgView.insertSubview(imageComparisonView, at: 0)
  76. // setViewBgImageNamed(named: "purchase_bj")
  77. contentView.addSubview(hostVc.view)
  78. hostVc.view.snp.makeConstraints { make in
  79. make.leading.trailing.bottom.top.equalToSuperview()
  80. }
  81. }
  82. override func dealThings() {
  83. //周会员和月会员不自动处理变化,必须点击购买后才处理
  84. let vipType = purchaseManager.vipType
  85. if vipType == .week || vipType == .month {
  86. isHandlePurchaseStateChanged = false
  87. }
  88. addNotifaction()
  89. onPurchaseStateChanged()
  90. NotificationCenter.default.addObserver(forName: .kPurchasePrepared, object: nil, queue: OperationQueue.main) { [weak self] _ in
  91. guard let self = self else { return }
  92. viewModel.selectedType = viewModel.selectedType
  93. }
  94. }
  95. func addNotifaction() {
  96. viewModel.buyPublisher.receive(on: DispatchQueue.main).sink { [weak self] _ in
  97. guard let self = self else {
  98. return
  99. }
  100. isHandlePurchaseStateChanged = true
  101. PurchaseManager.default.pay(for: self.viewModel.selectedType)
  102. }.store(in: &cancellabel)
  103. viewModel.privacyPublisher.receive(on: DispatchQueue.main).sink { [weak self] _ in
  104. guard let self = self else {
  105. return
  106. }
  107. let vc = TSBusinessWebVC(urlType: .privacy)
  108. vc.hidesBottomBarWhenPushed = true
  109. kPresentModalVC(target: self, modelVC: vc)
  110. }.store(in: &cancellabel)
  111. viewModel.termPublisher.receive(on: DispatchQueue.main).sink { [weak self] _ in
  112. guard let self = self else {
  113. return
  114. }
  115. let vc = TSBusinessWebVC(urlType: .terms)
  116. vc.hidesBottomBarWhenPushed = true
  117. kPresentModalVC(target: self, modelVC: vc)
  118. }.store(in: &cancellabel)
  119. viewModel.restorePublisher.receive(on: DispatchQueue.main).sink { [weak self] _ in
  120. guard let self = self else { return }
  121. isHandlePurchaseStateChanged = true
  122. PurchaseManager.default.restorePremium()
  123. }.store(in: &cancellabel)
  124. }
  125. func onPurchaseStateChanged(){
  126. purchaseManager.onPurchaseStateChanged = { [weak self] manager,state,object in
  127. guard let self = self else { return }
  128. if isHandlePurchaseStateChanged == false {
  129. debugPrint("purchaseManager.onPurchaseStateChanged 不处理")
  130. return
  131. }
  132. DispatchQueue.main.async {
  133. switch state {
  134. case .none:
  135. break
  136. case .loading:
  137. TSToastShared.showLoading(text: "Getting price".localized,containerView: self.view)
  138. case .loadSuccess:
  139. TSToastShared.hideLoading()
  140. case .loadFail:
  141. TSToastShared.hideLoading()
  142. let message = "Failed to get the price, will automatically retry in 5 seconds".localized
  143. TSToastShared.showToast(text: message)
  144. DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
  145. PurchaseManager.default.requestProducts()
  146. }
  147. case .paying:
  148. TSToastShared.showLoading(text: "Purchasing now".localized,containerView: self.view)
  149. case .paySuccess:
  150. TSToastShared.hideLoading()
  151. var loadingText = "Finish".localized
  152. if manager.isVip {
  153. loadingText = manager.vipType == .year ? "Congratulations on being VIP of the Year!".localized : "Congratulation you have become VIP".localized
  154. }
  155. TSToastShared.showToast(text:loadingText)
  156. if manager.isVip {
  157. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  158. self.closePage()
  159. }
  160. }
  161. case .payFail:
  162. TSToastShared.hideLoading()
  163. if let str = object as? String {
  164. TSToastShared.showToast(text: str)
  165. }
  166. case .restoreing:
  167. TSToastShared.showLoading(text: "Restoring now".localized,containerView: self.view)
  168. case .restoreSuccess:
  169. TSToastShared.hideLoading()
  170. let loadingText = manager.isVip ? "Congratulation you have become VIP".localized : "Couldn't Restore Subscription".localized
  171. debugPrint(loadingText)
  172. TSToastShared.showToast(text:loadingText)
  173. if manager.isVip {
  174. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  175. self.closePage()
  176. }
  177. }
  178. case .restoreFail:
  179. TSToastShared.hideLoading()
  180. let loadingText = (object as? String) ?? "Failed to restore subscribe, please try again".localized
  181. debugPrint(loadingText)
  182. TSToastShared.showToast(text: loadingText)
  183. case .verifying:
  184. #if DEBUG
  185. TSToastShared.showLoading(text: "Verifying receipt...".localized,containerView: self.view)
  186. #endif
  187. case .verifySuccess:
  188. break
  189. case .verifyFail:
  190. #if DEBUG
  191. TSToastShared.hideLoading()
  192. let message = (object as? String) ?? "Failed to validate receipt".localized
  193. TSToastShared.showToast(text:message)
  194. #endif
  195. }
  196. }
  197. debugPrint("PurchaseManager onPurchaseStateChanged=\(String(describing: state))")
  198. }
  199. }
  200. @objc func closePage(){
  201. closePageBlock?()
  202. TSToastShared.hideLoading()
  203. self.dismiss(animated: true)
  204. }
  205. override func viewWillAppear(_ animated: Bool) {
  206. super.viewWillAppear(animated)
  207. // 禁用右滑返回手势
  208. navigationController?.interactivePopGestureRecognizer?.isEnabled = false
  209. }
  210. override func viewWillDisappear(_ animated: Bool) {
  211. super.viewWillDisappear(animated)
  212. // 恢复右滑返回手势
  213. navigationController?.interactivePopGestureRecognizer?.isEnabled = true
  214. }
  215. deinit {
  216. cancellabel.removeAll()
  217. }
  218. }
  219. func kJudgeVipFreeType(vipFreeNumType:VipFreeNumType,
  220. vc:UIViewController? = nil,
  221. closePageBlock:(()->Void)? = nil) -> Bool {
  222. //判断 vip
  223. return kJudgeVip(externalBool: kPurchaseDefault.freeNumAvailable(type: vipFreeNumType) == false, vc: vc ,closePageBlock: closePageBlock)
  224. }
  225. func kJudgeVip(externalBool:Bool,
  226. vc:UIViewController? = nil,
  227. closePageBlock:(()->Void)? = nil) -> Bool {
  228. //判断 vip
  229. if externalBool,
  230. PurchaseManager.default.isVip == false
  231. {
  232. if let vc = vc {
  233. TSPurchaseVC.show(target: vc, closePageBlock: nil)
  234. }else if let rootVC = WindowHelper.getRootViewController() {
  235. TSPurchaseVC.show(target: rootVC, closePageBlock: nil)
  236. }
  237. return true
  238. }
  239. return false
  240. }
  241. extension TSPurchaseVC{
  242. static func show(target:UIViewController,closePageBlock:(()->Void)?){
  243. kDelayMainShort {
  244. let vc = TSPurchaseVC()
  245. vc.closePageBlock = closePageBlock
  246. let navi = TSBaseNavigationC(rootViewController: vc)
  247. navi.modalPresentationStyle = .overFullScreen
  248. target.present(navi, animated: true)
  249. }
  250. }
  251. }
  252. struct PurchaseView :View {
  253. @ObservedObject var viewModel: PurchaseViewModel
  254. var body: some View {
  255. let vipType = PurchaseManager.default.vipType
  256. VStack {
  257. Spacer()
  258. VStack {
  259. let text = vipType == .none ? "Get PRO Access".localized : "Super Offer for Yearly Pro".localized
  260. Text(text)
  261. .multilineTextAlignment(.center)
  262. .font(.font(name: .PoppinsBoldItalic,size: 26))
  263. .foregroundColor(UIColor.white.color)
  264. .frame(width: k_ScreenWidth - 32, alignment: .center)
  265. if vipType == .none {
  266. Spacer().frame(height: 12)
  267. HStack {
  268. Text("Unlimited")
  269. .foregroundColor("#FECB34".uiColor.color)
  270. Text("Generation")
  271. .foregroundColor(UIColor.white.color)
  272. }
  273. .multilineTextAlignment(.center)
  274. .font(.font(name: .PoppinsBoldItalic,size: 26))
  275. .frame(height: 26*kDesignScale)
  276. }
  277. }
  278. Spacer().frame(height: 32)
  279. VStack(spacing: 12) {
  280. if vipType == .none {
  281. ZStack(alignment: .topTrailing) {
  282. PurchaseItemView(title: "One Month".localized, type: .month, selectedType: $viewModel.selectedType).onTapGesture {
  283. viewModel.selectedType = .month
  284. }
  285. TSVipRecView(save: vipType.saveString)
  286. .offset(x:-30,y:-14)
  287. }
  288. PurchaseItemView(title: "One Week".localized, type: .week, selectedType: $viewModel.selectedType).onTapGesture {
  289. viewModel.selectedType = .week
  290. }
  291. }else{
  292. PurchaseItemTypeOneView(title: "One Year".localized, type: .year, selectedType: $viewModel.selectedType).onTapGesture {
  293. viewModel.selectedType = .year
  294. }
  295. }
  296. Spacer().frame(height: 4)
  297. Button {
  298. viewModel.buyPublisher.send(true)
  299. } label: {
  300. ZStack {
  301. UIColor.themeColor.color
  302. Text("Continue")
  303. .font(.font(size: 16,weight: .medium))
  304. .foregroundColor(.hex("#111111"))
  305. }.frame(maxWidth: .infinity ,minHeight: 48.0,maxHeight: 48.0)
  306. .cornerRadius(24.0)
  307. }
  308. HStack {
  309. Text("Recurring billing, cancel anytime".localized)
  310. .foregroundColor(Color.hex("#FFBD59")) +
  311. 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)
  312. .foregroundColor(UIColor.lesserText.color)
  313. }
  314. .multilineTextAlignment(.center).font(.font(size: 8))
  315. .onTapGesture {
  316. viewModel.privacyPublisher.send(true)
  317. }
  318. Spacer().frame(height: 6.0)
  319. HStack(spacing: 8) {
  320. Text("Terms of us".localized)
  321. .onTapGesture {
  322. viewModel.termPublisher.send(true)
  323. }
  324. Text("|")
  325. Text("Privacy Policy".localized)
  326. .onTapGesture {
  327. viewModel.privacyPublisher.send(true)
  328. }
  329. Text("|")
  330. Text("Restore".localized)
  331. .onTapGesture {
  332. viewModel.restorePublisher.send(true)
  333. }
  334. }.font(.system(size: 12)).foregroundColor(.hex("#999999"))
  335. }.padding(.horizontal)
  336. Spacer().frame(height:9+k_Height_safeAreaInsetsBottom())
  337. }
  338. }
  339. // 定义一个返回 View 的方法
  340. func customText(text:String,fontName:FontName,color:Color) -> some View {
  341. let gorgeousColor = color //UIColor.themeColor.color
  342. return Text(text)
  343. .font(.font(name: fontName,size: 48))
  344. .gradientForeground(
  345. colors: [.hex("#FA794F"),.hex("#F8C32A"),.hex("#FEFBF4")],
  346. startPoint: UnitPoint.leading,
  347. endPoint: UnitPoint.trailing
  348. )
  349. .foregroundColor(gorgeousColor)
  350. .frame(height: 20)
  351. }
  352. }
  353. struct PurchaseItemView: View {
  354. var title: String
  355. var type: PremiumPeriod
  356. @Binding var selectedType: PremiumPeriod
  357. var body: some View {
  358. ZStack {
  359. Color.white.opacity(0.1)
  360. HStack {
  361. //左边加个
  362. VStack(alignment: .leading, spacing: 14) {
  363. Text(title).font(.font(size: 14)).foregroundColor(UIColor.white.color)
  364. Text(PurchaseManager.default.price(for: type) ?? "--").font(.font(size: 18,weight: .medium)).foregroundColor(UIColor.mainText.color)
  365. }
  366. Spacer()
  367. //右边每周的💰
  368. VStack(alignment: .trailing, spacing: 2) {
  369. Text("\(PurchaseManager.default.averageWeekly(for:type) ?? "--")")
  370. Text("Per week".localized)
  371. }.font(.font(size: 16,weight: .regular)).foregroundColor(Color.white.opacity(0.6))
  372. }.padding(.horizontal)
  373. }
  374. .frame(height: 74) // 设置高度
  375. .cornerRadius(16.0) // 圆角
  376. .overlay(
  377. RoundedRectangle(cornerRadius: 16)
  378. .stroke(Color.hex("#FECB34"), lineWidth: type == selectedType ? 1 : 0) // 边框
  379. )
  380. }
  381. }
  382. struct PurchaseItemTypeOneView: View {
  383. var title: String
  384. var type: PremiumPeriod
  385. @Binding var selectedType: PremiumPeriod
  386. var body: some View {
  387. ZStack {
  388. Color.white.opacity(0.1)
  389. HStack {
  390. //左边加个
  391. VStack(alignment: .leading, spacing: 8) {
  392. Text(title).font(.font(size: 14,weight: .medium)).foregroundColor(UIColor.white.color)
  393. Text(String(format:"Only %@ per day".localized,"\(PurchaseManager.default.averageDay(for:type) ?? "--")"))
  394. .font(.font(size: 12,weight: .medium))
  395. . foregroundColor(UIColor.white.color).opacity(0.7)
  396. }
  397. Spacer()
  398. Text(PurchaseManager.default.price(for: type) ?? "--").font(.font(size: 18,weight: .medium)).foregroundColor(UIColor.mainText.color)
  399. }.padding(.horizontal)
  400. }
  401. .frame(height: 74) // 设置高度
  402. .cornerRadius(16.0) // 圆角
  403. .overlay(
  404. RoundedRectangle(cornerRadius: 16)
  405. .stroke(Color.hex("#FECB34"), lineWidth: type == selectedType ? 1 : 0) // 边框
  406. )
  407. }
  408. }
  409. //推荐选择view
  410. struct TSVipRecView: View {
  411. var save:String //年 80%,月 30%
  412. var body: some View {
  413. HStack(spacing: 4) {
  414. Image("upvote_black").resizable().frame(width: 16, height: 16)
  415. Text("Save-Vip".localized + " " + save).font(.font(size: 12,weight: .medium)).foregroundColor(.hex("#111111"))
  416. }
  417. .padding(EdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6))
  418. .background(Color.hex("#FECB34"))
  419. .frame(height: 28) // 设置高度
  420. .cornerRadius([.topLeading, .topTrailing, .bottomLeading, .bottomTrailing], 16.0)
  421. }
  422. }