TSPurchaseVC.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  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//.lifetime
  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. lazy var hostVc: UIHostingController<PurchaseView> = {
  31. let vc = UIHostingController(rootView: PurchaseView(viewModel: viewModel))
  32. vc.view.backgroundColor = .clear
  33. return vc
  34. }()
  35. lazy var imageComparisonView: TYCycleImageComparisonView = {
  36. let imageComparisonView = TYCycleImageComparisonView()
  37. imageComparisonView.frame = CGRect(x: 0, y: 0, width: k_ScreenWidth, height: 532*kDesignScale)
  38. imageComparisonView.titleArray = ["Descratch".localized,"Colorize".localized,"Enhance".localized,"Recreate".localized]
  39. imageComparisonView.itemModelArray = [
  40. TSImageComparisonModel(oldImage: UIImage(named: "image_comparison_old_0")!, newImage: UIImage(named: "image_comparison_new_0")!),
  41. TSImageComparisonModel(oldImage: UIImage(named: "image_comparison_old_1")!, newImage: UIImage(named: "image_comparison_new_1")!),
  42. TSImageComparisonModel(oldImage: UIImage(named: "image_comparison_old_2")!, newImage: UIImage(named: "image_comparison_new_2")!),
  43. TSImageComparisonModel(oldImage: UIImage(named: "image_comparison_old_3")!, newImage: UIImage(named: "image_comparison_new_3")!)
  44. ]
  45. return imageComparisonView
  46. }()
  47. override func createView() {
  48. addNormalNavBarView()
  49. _ = setNavigationItem("", imageName: "close_gray", direction: .left, action: #selector(closePage))
  50. setViewBgImageNamed(named: "purchase_bg_shade")
  51. view.insertSubview(imageComparisonView, at: 0)
  52. contentView.addSubview(hostVc.view)
  53. hostVc.view.snp.makeConstraints { make in
  54. make.leading.trailing.bottom.top.equalToSuperview()
  55. }
  56. }
  57. override func dealThings() {
  58. PurchaseManager.default.requestProducts()
  59. addNotifaction()
  60. onPurchaseStateChanged()
  61. }
  62. func addNotifaction() {
  63. viewModel.buyPublisher.receive(on: DispatchQueue.main).sink { [weak self] _ in
  64. guard let self = self else {
  65. return
  66. }
  67. PurchaseManager.default.pay(for: self.viewModel.selectedType)
  68. }.store(in: &cancellabel)
  69. viewModel.privacyPublisher.receive(on: DispatchQueue.main).sink { [weak self] _ in
  70. guard let self = self else {
  71. return
  72. }
  73. let vc = TSBusinessWebVC(urlType: .privacy)
  74. vc.hidesBottomBarWhenPushed = true
  75. kPresentModalVC(target: self, modelVC: vc)
  76. }.store(in: &cancellabel)
  77. viewModel.termPublisher.receive(on: DispatchQueue.main).sink { [weak self] _ in
  78. guard let self = self else {
  79. return
  80. }
  81. let vc = TSBusinessWebVC(urlType: .terms)
  82. vc.hidesBottomBarWhenPushed = true
  83. kPresentModalVC(target: self, modelVC: vc)
  84. }.store(in: &cancellabel)
  85. viewModel.restorePublisher.receive(on: DispatchQueue.main).sink { _ in
  86. PurchaseManager.default.restorePremium()
  87. }.store(in: &cancellabel)
  88. }
  89. func onPurchaseStateChanged(){
  90. purchaseManager.onPurchaseStateChanged = { [weak self] manager,state,object in
  91. guard let self = self else { return }
  92. DispatchQueue.main.async {
  93. switch state {
  94. case .none:
  95. break
  96. case .loading:
  97. TSToastShared.showLoading(text: "Getting price".localized,containerView: self.view)
  98. case .loadSuccess:
  99. TSToastShared.hideLoading()
  100. case .loadFail:
  101. TSToastShared.hideLoading()
  102. let message = "Get price failure, Will automatically retry in 5 seconds".localized
  103. TSToastShared.showToast(text: message)
  104. DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
  105. PurchaseManager.default.requestProducts()
  106. }
  107. case .paying:
  108. TSToastShared.showLoading(text: "Purchasing now".localized,containerView: self.view)
  109. case .paySuccess:
  110. TSToastShared.hideLoading()
  111. let loadingText = manager.isVip ? "Congratulation you have become VIP".localized : "Finish".localized
  112. TSToastShared.showToast(text:loadingText)
  113. if manager.isVip {
  114. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  115. self.closePage()
  116. }
  117. }
  118. case .payFail:
  119. TSToastShared.hideLoading()
  120. if let str = object as? String {
  121. TSToastShared.showToast(text: str)
  122. }
  123. case .restoreing:
  124. TSToastShared.showLoading(text: "Restoring now".localized,containerView: self.view)
  125. case .restoreSuccess:
  126. TSToastShared.hideLoading()
  127. let loadingText = manager.isVip ? "Congratulation you have become VIP".localized : "Couldn't Restore Subscription".localized
  128. debugPrint(loadingText)
  129. TSToastShared.showToast(text:loadingText)
  130. if manager.isVip {
  131. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  132. self.closePage()
  133. }
  134. }
  135. case .restoreFail:
  136. TSToastShared.hideLoading()
  137. let loadingText = (object as? String) ?? "Failed to restore subscribe, please try again".localized
  138. debugPrint(loadingText)
  139. TSToastShared.showToast(text: loadingText)
  140. case .verifying:
  141. #if DEBUG
  142. TSToastShared.showLoading(text: "Verifying receipt...".localized,containerView: self.view)
  143. #endif
  144. case .verifySuccess:
  145. break
  146. case .verifyFail:
  147. #if DEBUG
  148. TSToastShared.hideLoading()
  149. let message = (object as? String) ?? "Verify receipt failed".localized()
  150. TSToastShared.showToast(text:message)
  151. #endif
  152. }
  153. }
  154. debugPrint("PurchaseManager onPurchaseStateChanged=\(String(describing: state))")
  155. }
  156. }
  157. @objc func closePage(){
  158. closePageBlock?()
  159. TSToastShared.hideLoading()
  160. self.dismiss(animated: true)
  161. }
  162. deinit {
  163. cancellabel.removeAll()
  164. }
  165. }
  166. extension TSPurchaseVC{
  167. static func show(target:UIViewController,closePageBlock:(()->Void)? = nil){
  168. let vc = TSPurchaseVC()
  169. vc.closePageBlock = closePageBlock
  170. let navi = TSBaseNavigationC(rootViewController: vc)
  171. navi.modalPresentationStyle = .overFullScreen
  172. target.present(navi, animated: true)
  173. }
  174. }
  175. struct PurchaseView :View {
  176. @ObservedObject var viewModel: PurchaseViewModel
  177. var body: some View {
  178. let vipType = PurchaseManager.default.vipType
  179. VStack {
  180. Spacer()
  181. VStack {
  182. // let text = vipType == .none ? "Get PRO Access".localized : "Super Offer for Yearly Pro".localized
  183. // 自定义颜色
  184. // HighlightedText("Get PRO Access",
  185. // highlighted: "PRO",
  186. // baseColor: .white,
  187. // highlightColor: UIColor.themeColor.color)
  188. Text("Get PRO Access")
  189. .foregroundStyle(.white)
  190. .multilineTextAlignment(.center)
  191. .font(.font(name: .ZillaSlabBoldItalic,size: 36))
  192. .frame(width: k_ScreenWidth - 32, alignment: .center)
  193. Spacer().frame(height: 16)
  194. ZStack {
  195. VStack(alignment: .leading,spacing: 8) {
  196. HStack(spacing: 8) {
  197. Spacer().frame(width: 34)
  198. Image(.purchaseIconUnlimited).resizable().frame(width: 24, height: 24)
  199. Text("Unlimited Restore Photos".localized())
  200. Spacer()
  201. }
  202. HStack(spacing: 8) {
  203. Spacer().frame(width: 34)
  204. Image(.purchaseIconHd).resizable().frame(width: 24, height: 24)
  205. Text("Enhance & Colorize at High Quality".localized())
  206. Spacer()
  207. }
  208. HStack(spacing: 8) {
  209. Spacer().frame(width: 34)
  210. Image(.purchaseIconAd).resizable().frame(width: 24, height: 24)
  211. Text("100% No Ads".localized()).multilineTextAlignment(.leading)
  212. Spacer()
  213. }
  214. }.font(.font(size: 16)).foregroundColor(.white)
  215. }
  216. }
  217. Spacer().frame(height: 20)
  218. VStack(spacing: 12) {
  219. // if vipType == .none {
  220. ZStack(alignment: .topTrailing) {
  221. // //增加月付费
  222. // PurchaseItemView(title: "One Month".localized, type: .month, selectedType: $viewModel.selectedType).onTapGesture {
  223. // viewModel.selectedType = .month
  224. PurchaseItemView(title: "YEARLY".localized, type: .year, selectedType: $viewModel.selectedType).onTapGesture {
  225. viewModel.selectedType = .year
  226. viewModel.buyPublisher.send(true)
  227. }
  228. TSVipRecView(save: vipType.saveString)
  229. .offset(x:-30,y:-14)
  230. }
  231. PurchaseItemView(title: "MONTH".localized, type: .month, selectedType: $viewModel.selectedType).onTapGesture {
  232. viewModel.selectedType = .month
  233. viewModel.buyPublisher.send(true)
  234. }
  235. // }else{
  236. // PurchaseItemTypeOneView(title: "One Year".localized, type: .year, selectedType: $viewModel.selectedType).onTapGesture {
  237. // viewModel.selectedType = .year
  238. // viewModel.buyPublisher.send(true)
  239. // }
  240. // }
  241. Button {
  242. viewModel.buyPublisher.send(true)
  243. } label: {
  244. ZStack {
  245. Image("submit_btn_bg").resizable().aspectRatio(contentMode: .fill)
  246. Text("Continue")
  247. .font(.system(size: 16))
  248. .foregroundColor(.hex("#111111"))
  249. }.frame(maxWidth: .infinity ,maxHeight: 48.0)
  250. .cornerRadius(0.0)
  251. }
  252. HStack {
  253. Text("Recurring billing, cancel anytime")
  254. .foregroundColor(UIColor.themeColor.color) +
  255. 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.")
  256. .foregroundColor(UIColor.lesserText.color)
  257. }
  258. .multilineTextAlignment(.center).font(.font(size: 8))
  259. .onTapGesture {
  260. viewModel.privacyPublisher.send(true)
  261. }
  262. HStack(spacing: 8) {
  263. Text("Term of us")
  264. .onTapGesture {
  265. viewModel.termPublisher.send(true)
  266. }
  267. Text("|")
  268. Text("Privacy Policy")
  269. .onTapGesture {
  270. viewModel.privacyPublisher.send(true)
  271. }
  272. Text("|")
  273. Text("Restore")
  274. .onTapGesture {
  275. viewModel.restorePublisher.send(true)
  276. }
  277. }.font(.system(size: 12)).foregroundColor(.hex("#999999"))
  278. }.padding(.horizontal)
  279. Spacer().frame(height:9+k_Height_safeAreaInsetsBottom())
  280. }
  281. }
  282. }
  283. struct PurchaseItemView: View {
  284. var title: String
  285. var type: PremiumPeriod
  286. @Binding var selectedType: PremiumPeriod
  287. var body: some View {
  288. let textColor = Color.white.opacity(0.6)
  289. ZStack {
  290. Color.white.opacity(0.1)
  291. HStack {
  292. //左边加个
  293. VStack(alignment: .leading, spacing: 14) {
  294. Text(title).font(.font(size: 16,weight: .medium)).foregroundColor(type == selectedType ? UIColor.themeColor.color : textColor)
  295. HStack{
  296. Text("Just".localized)
  297. Text(PurchaseManager.default.price(for: type) ?? "--").font(.font(size: 18))
  298. Text(perString())
  299. }.font(.font(size: 14))
  300. .foregroundColor(type == selectedType ? .white : textColor)
  301. }
  302. Spacer()
  303. //右边每周的💰
  304. VStack(alignment: .trailing, spacing: 2) {
  305. Text("\(PurchaseManager.default.averageWeekly(for:type) ?? "--")")
  306. Text("Per week".localized)
  307. }.font(.font(size: 16,weight: .regular)).foregroundColor(Color.white.opacity(0.6))
  308. }.padding(.horizontal)
  309. }
  310. .foregroundColor(textColor)
  311. .frame(height: 74) // 设置高度
  312. .overlay(
  313. RoundedRectangle(cornerRadius: 0)
  314. .stroke(UIColor.themeColor.color, lineWidth: type == selectedType ? 1 : 0) // 边框
  315. )
  316. }
  317. // func priceString()->String{
  318. // switch type {
  319. // case .year:
  320. // return "Just \(PurchaseManager.default.price(for: type) ?? "--") Per Year"
  321. // case .month:
  322. // return "Just \(PurchaseManager.default.price(for: type) ?? "--") Per Week"
  323. // default:
  324. // return "--"
  325. // }
  326. // }
  327. func perString()->String{
  328. switch type {
  329. case .year:
  330. return "Per Year".localized
  331. case .month:
  332. return "Per Month".localized
  333. default:
  334. return ""
  335. }
  336. }
  337. }
  338. //推荐选择view
  339. struct TSVipRecView: View {
  340. var save:String
  341. var body: some View {
  342. HStack(spacing: 4) {
  343. // Image("upvote_black").resizable().frame(width: 16, height: 16)
  344. Text("Save".localized + " " + save ).font(.font(size: 12,weight: .medium)).foregroundColor(.hex("#111111"))
  345. }
  346. .padding(EdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6))
  347. // .background(Color.hex("#FECB34"))
  348. // .background(
  349. .linearGradientBackground(colors: ["#F1D3AB".uiColor.color,"#E4A858".uiColor.color])
  350. .frame(height: 24) // 设置高度
  351. .cornerRadius(12.0)
  352. // .cornerRadius([.topLeading, .topTrailing, .bottomLeading, .bottomTrailing], 16.0)
  353. }
  354. }
  355. extension View {
  356. /// 添加线性渐变背景
  357. /// - Parameters:
  358. /// - colors: 渐变颜色数组
  359. /// - startPoint: 渐变起点 (默认 .top)
  360. /// - endPoint: 渐变终点 (默认 .bottom)
  361. /// - cornerRadius: 圆角半径 (默认 0)
  362. func linearGradientBackground(colors: [Color],
  363. startPoint: UnitPoint = .leading,
  364. endPoint: UnitPoint = .trailing,
  365. cornerRadius: CGFloat = 0) -> some View {
  366. self.background(
  367. LinearGradient(
  368. gradient: Gradient(colors: colors),
  369. startPoint: startPoint,
  370. endPoint: endPoint
  371. )
  372. .cornerRadius(cornerRadius)
  373. )
  374. }
  375. }