TSPurchaseVC.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  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 = kPurchaseDefault
  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 closeBtn: UIButton = {
  36. let closeBtn = UIButton.createButton(image: UIImage(named: "close_gray")){ [weak self] in
  37. guard let self = self else { return }
  38. closePage()
  39. }
  40. return closeBtn
  41. }()
  42. override func createView() {
  43. // addNormalNavBarView()
  44. // _ = setNavigationItem("", imageName: "close_gray", direction: .left, action: #selector(closePage))
  45. setNavBarViewHidden(true)
  46. setViewBgImageNamed(named: "purchase_bj")
  47. contentView.addSubview(hostVc.view)
  48. hostVc.view.snp.makeConstraints { make in
  49. make.leading.trailing.bottom.top.equalToSuperview()
  50. }
  51. contentView.addSubview(closeBtn)
  52. closeBtn.snp.makeConstraints { make in
  53. make.leading.equalTo(10)
  54. make.top.equalTo(k_Height_StatusBar + 4)
  55. make.width.height.equalTo(36)
  56. }
  57. }
  58. override func dealThings() {
  59. addNotifaction()
  60. onPurchaseStateChanged()
  61. NotificationCenter.default.addObserver(forName: .kPurchasePrepared, object: nil, queue: OperationQueue.main) { [weak self] _ in
  62. guard let self = self else { return }
  63. viewModel.selectedType = viewModel.selectedType
  64. }
  65. }
  66. func addNotifaction() {
  67. viewModel.buyPublisher.receive(on: DispatchQueue.main).sink { [weak self] _ in
  68. guard let self = self else {
  69. return
  70. }
  71. kPurchaseDefault.pay(for: self.viewModel.selectedType)
  72. }.store(in: &cancellabel)
  73. viewModel.privacyPublisher.receive(on: DispatchQueue.main).sink { [weak self] _ in
  74. guard let self = self else {
  75. return
  76. }
  77. let vc = TSBusinessWebVC(urlType: .privacy)
  78. vc.hidesBottomBarWhenPushed = true
  79. kPresentModalVC(target: self, modelVC: vc)
  80. }.store(in: &cancellabel)
  81. viewModel.termPublisher.receive(on: DispatchQueue.main).sink { [weak self] _ in
  82. guard let self = self else {
  83. return
  84. }
  85. let vc = TSBusinessWebVC(urlType: .terms)
  86. vc.hidesBottomBarWhenPushed = true
  87. kPresentModalVC(target: self, modelVC: vc)
  88. }.store(in: &cancellabel)
  89. viewModel.restorePublisher.receive(on: DispatchQueue.main).sink { _ in
  90. kPurchaseDefault.restorePremium()
  91. }.store(in: &cancellabel)
  92. }
  93. func onPurchaseStateChanged(){
  94. purchaseManager.onPurchaseStateChanged = { [weak self] manager,state,object in
  95. guard let self = self else { return }
  96. DispatchQueue.main.async {
  97. switch state {
  98. case .none:
  99. break
  100. case .loading:
  101. TSToastShared.showLoading(text: "Getting price".localized,containerView: self.view)
  102. case .loadSuccess:
  103. TSToastShared.hideLoading()
  104. case .loadFail:
  105. TSToastShared.hideLoading()
  106. let message = "Get price failure, Will automatically retry in 5 seconds".localized
  107. TSToastShared.showToast(text: message)
  108. DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
  109. kPurchaseDefault.requestProducts()
  110. }
  111. case .paying:
  112. TSToastShared.showLoading(text: "Purchasing now".localized,containerView: self.view)
  113. case .paySuccess:
  114. TSToastShared.hideLoading()
  115. let loadingText = manager.isVip ? "Congratulation you have become VIP".localized : "Finish".localized
  116. TSToastShared.showToast(text:loadingText)
  117. if manager.isVip {
  118. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  119. self.closePage()
  120. }
  121. }
  122. case .payFail:
  123. TSToastShared.hideLoading()
  124. if let str = object as? String {
  125. TSToastShared.showToast(text: str)
  126. }
  127. case .restoreing:
  128. TSToastShared.showLoading(text: "Restoring now".localized,containerView: self.view)
  129. case .restoreSuccess:
  130. TSToastShared.hideLoading()
  131. let loadingText = manager.isVip ? "Congratulation you have become VIP".localized : "Couldn't Restore Subscription".localized
  132. debugPrint(loadingText)
  133. TSToastShared.showToast(text:loadingText)
  134. if manager.isVip {
  135. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  136. self.closePage()
  137. }
  138. }
  139. case .restoreFail:
  140. TSToastShared.hideLoading()
  141. let loadingText = (object as? String) ?? "Failed to restore subscribe, please try again".localized
  142. debugPrint(loadingText)
  143. TSToastShared.showToast(text: loadingText)
  144. case .verifying:
  145. #if DEBUG
  146. TSToastShared.showLoading(text: "Verifying receipt...".localized,containerView: self.view)
  147. #endif
  148. case .verifySuccess:
  149. break
  150. case .verifyFail:
  151. #if DEBUG
  152. TSToastShared.hideLoading()
  153. let message = (object as? String) ?? "Verify receipt failed"
  154. TSToastShared.showToast(text:message)
  155. #endif
  156. }
  157. }
  158. debugPrint("PurchaseManager onPurchaseStateChanged=\(String(describing: state))")
  159. }
  160. }
  161. @objc func closePage(){
  162. closePageBlock?()
  163. TSToastShared.hideLoading()
  164. self.dismiss(animated: true)
  165. }
  166. override func viewWillAppear(_ animated: Bool) {
  167. super.viewWillAppear(animated)
  168. // 禁用右滑返回手势
  169. navigationController?.interactivePopGestureRecognizer?.isEnabled = false
  170. }
  171. override func viewWillDisappear(_ animated: Bool) {
  172. super.viewWillDisappear(animated)
  173. // 恢复右滑返回手势
  174. navigationController?.interactivePopGestureRecognizer?.isEnabled = true
  175. }
  176. deinit {
  177. cancellabel.removeAll()
  178. }
  179. }
  180. extension TSPurchaseVC{
  181. static func show(target:UIViewController,closePageBlock:(()->Void)?){
  182. let vc = TSPurchaseVC()
  183. vc.closePageBlock = closePageBlock
  184. let navi = TSBaseNavigationC(rootViewController: vc)
  185. navi.modalPresentationStyle = .fullScreen//.overFullScreen
  186. target.present(navi, animated: true)
  187. }
  188. }
  189. struct PurchaseView :View {
  190. @ObservedObject var viewModel: PurchaseViewModel
  191. var body: some View {
  192. ScrollView {
  193. Spacer().frame(height:k_Height_StatusBar + 6)
  194. VStack {
  195. Image("vip_big_icon").resizable().frame(width: 163, height: 163)
  196. Text(" Ringtones Pro")
  197. .font(.font(name: .PoppinsBlackItalic,size: 30))
  198. .gradientForeground(
  199. colors: [Color.white, "#F7B7FF".uiColor.color],
  200. startPoint: UnitPoint.top,
  201. endPoint: UnitPoint.bottom
  202. )
  203. .frame(height: 30)
  204. Spacer().frame(height: 36)
  205. ZStack {
  206. VStack(alignment: .leading,spacing: 20) {
  207. let w = 28.0
  208. let h = 28.0
  209. HStack(spacing: 16) {
  210. Image("vip_ringtone").resizable().frame(width: w, height: h)
  211. Text("Unlimited ringtones generation")
  212. }
  213. HStack(spacing: 16) {
  214. Image("vip_pic").resizable().frame(width: w, height: h)
  215. Text("Generate unlimited contact poster&photo")
  216. }
  217. HStack(spacing: 16) {
  218. Image("vip_photo").resizable().frame(width: w, height: h)
  219. Text("Unlock all premium calling theme")
  220. }
  221. HStack(spacing: 16) {
  222. Image("vip_ads").resizable().frame(width: w, height: h)
  223. Text("100% No Ads").multilineTextAlignment(.leading)
  224. }
  225. }.font(.font(size: 14)).foregroundColor(UIColor.white.color)
  226. }
  227. }
  228. Spacer().frame(height: 22)
  229. VStack(spacing: 12) {
  230. ZStack(alignment: .topTrailing) {
  231. PurchaseItemView(title: "One Year", type: .year, selectedType: $viewModel.selectedType).onTapGesture {
  232. viewModel.selectedType = .year
  233. }
  234. TSVipRecView().offset(y:-14)
  235. }
  236. ZStack(alignment: .topTrailing) {
  237. PurchaseItemView(title: "One Month", type: .month, selectedType: $viewModel.selectedType).onTapGesture {
  238. viewModel.selectedType = .month
  239. }
  240. }
  241. // HStack {
  242. // PurchaseItemView(title: "One Year", type: .year, selectedType: $viewModel.selectedType).onTapGesture {
  243. // viewModel.selectedType = .year
  244. // }
  245. //
  246. // PurchaseItemView(title: "One Week", type: .week, selectedType: $viewModel.selectedType).onTapGesture {
  247. // viewModel.selectedType = .week
  248. // }
  249. // }
  250. Spacer().frame(height: 2)
  251. Button {
  252. viewModel.buyPublisher.send(true)
  253. } label: {
  254. ZStack {
  255. Color.hex("#E661F6").frame(height: 48)
  256. // "#E661F6".uiColor.color
  257. // Image("submit_btn_bg").resizable().aspectRatio(contentMode: .fill)
  258. Text("Continue")
  259. .font(.system(size: 16))
  260. .foregroundColor(.white)
  261. }.frame(maxWidth: .infinity ,maxHeight: 48.0)
  262. .cornerRadius(24.0)
  263. }
  264. HStack {
  265. Text("Recurring billing, cancel anytime")
  266. .foregroundColor(Color.hex("#E661F6")) +
  267. 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.")
  268. .foregroundColor(UIColor.lesserText.color)
  269. }
  270. .multilineTextAlignment(.center).font(.font(size: 8))
  271. .onTapGesture {
  272. viewModel.privacyPublisher.send(true)
  273. }
  274. HStack(spacing: 8) {
  275. Text("Term of us")
  276. .onTapGesture {
  277. viewModel.termPublisher.send(true)
  278. }
  279. Text("|")
  280. Text("Privacy Policy")
  281. .onTapGesture {
  282. viewModel.privacyPublisher.send(true)
  283. }
  284. Text("|")
  285. Text("Restore")
  286. .onTapGesture {
  287. viewModel.restorePublisher.send(true)
  288. }
  289. }.font(.system(size: 12)).foregroundColor(.hex("#999999"))
  290. }.padding(.horizontal)
  291. }
  292. }
  293. }
  294. struct PurchaseItemView: View {
  295. var title: String
  296. var type: PremiumPeriod
  297. @Binding var selectedType: PremiumPeriod
  298. var body: some View {
  299. let themeColor = Color.hex("#E661F6")
  300. let fontColor = type == selectedType ? UIColor.mainText.color : UIColor.textAssist.color
  301. ZStack {
  302. if type == selectedType {
  303. themeColor.opacity(0.1)
  304. }else{
  305. Color.white.opacity(0.1)
  306. }
  307. HStack {
  308. VStack(alignment: .leading, spacing: 8) {
  309. Text(title).font(.font(size: 14)).foregroundColor(fontColor)
  310. Text(kPurchaseDefault.price(for: type) ?? "--").font(.font(size: 18,weight: .medium)).foregroundColor(fontColor)
  311. }
  312. Spacer()
  313. if type == selectedType {
  314. Image(.radioboxSelected)
  315. }
  316. }.padding(.horizontal)
  317. }
  318. .frame(height: 74) // 设置高度
  319. .cornerRadius(16.0) // 圆角
  320. .overlay(
  321. RoundedRectangle(cornerRadius: 16)
  322. .stroke(themeColor, lineWidth: type == selectedType ? 1 : 0) // 边框
  323. )
  324. .shadow(color: type == selectedType ? themeColor : Color.clear, radius: 4, x: 0, y: 1)
  325. }
  326. }
  327. //推荐选择view
  328. struct TSVipRecView: View {
  329. var body: some View {
  330. HStack(spacing: 4) {
  331. Image("upvote_black").resizable().frame(width: 16, height: 16)
  332. Text("Save 60%").font(.font(size: 12,weight: .medium)).foregroundColor(.white)
  333. }
  334. .padding(EdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6))
  335. .background(Color.hex("#E661F6"))
  336. .frame(height: 28) // 设置高度
  337. .cornerRadius([.topLeading, .topTrailing, .bottomLeading], 16.0)
  338. }
  339. }