TSPurchaseVC.swift 14 KB

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