TSPurchaseVC.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  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 = 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. 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. PurchaseManager.default.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. PurchaseManager.default.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. PurchaseManager.default.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)?) -> Bool {
  155. //判断 vip
  156. if externalBool,
  157. PurchaseManager.default.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: 263, height: 63)
  180. Spacer().frame(height: 16)
  181. Text("Premium")
  182. .font(.font(size: 16))
  183. .padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20))
  184. .frame(height: 34)
  185. .foregroundColor(.white)
  186. .background(Color.white.opacity(0.1))
  187. .cornerRadius(17.0)
  188. Spacer().frame(height: 36)
  189. ZStack {
  190. VStack(alignment: .leading,spacing: 8) {
  191. HStack(spacing: 8) {
  192. Image("check").resizable().frame(width: 24, height: 24)
  193. Text("Unlimited Wallpaper")
  194. }
  195. HStack(spacing: 8) {
  196. Image("check").resizable().frame(width: 24, height: 24)
  197. Text("Unlimited AI Emoji")
  198. }
  199. HStack(spacing: 8) {
  200. Image("check").resizable().frame(width: 24, height: 24)
  201. Text("100% No Ads").multilineTextAlignment(.leading)
  202. }
  203. }.font(.font(size: 16)).foregroundColor(UIColor.lesserText.color)
  204. }
  205. }
  206. Spacer().frame(height: 25)
  207. VStack(spacing: 12) {
  208. ZStack(alignment: .topTrailing) {
  209. PurchaseItemView(title: "One Month", type: .month, selectedType: $viewModel.selectedType).onTapGesture {
  210. viewModel.selectedType = .month
  211. }
  212. TSVipRecView()
  213. .offset(y:-14)
  214. }
  215. HStack {
  216. PurchaseItemView(title: "One Year", type: .year, selectedType: $viewModel.selectedType).onTapGesture {
  217. viewModel.selectedType = .year
  218. }
  219. PurchaseItemView(title: "One Week", type: .week, selectedType: $viewModel.selectedType).onTapGesture {
  220. viewModel.selectedType = .week
  221. }
  222. }
  223. Button {
  224. viewModel.buyPublisher.send(true)
  225. } label: {
  226. ZStack {
  227. Image("submit_btn_bg").resizable().aspectRatio(contentMode: .fill)
  228. Text("Continue")
  229. .font(.system(size: 16))
  230. .foregroundColor(.hex("#111111"))
  231. }.frame(maxWidth: .infinity ,maxHeight: 48.0)
  232. .cornerRadius(24.0)
  233. }
  234. HStack {
  235. Text("Recurring billing, cancel anytime")
  236. .foregroundColor(Color.hex("#FFBD59")) +
  237. 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.")
  238. .foregroundColor(UIColor.lesserText.color)
  239. }
  240. .multilineTextAlignment(.center).font(.font(size: 8))
  241. .onTapGesture {
  242. viewModel.privacyPublisher.send(true)
  243. }
  244. HStack(spacing: 8) {
  245. Text("Term of us")
  246. .onTapGesture {
  247. viewModel.termPublisher.send(true)
  248. }
  249. Text("|")
  250. Text("Privacy Policy")
  251. .onTapGesture {
  252. viewModel.privacyPublisher.send(true)
  253. }
  254. Text("|")
  255. Text("Restore")
  256. .onTapGesture {
  257. viewModel.restorePublisher.send(true)
  258. }
  259. }.font(.system(size: 12)).foregroundColor(.hex("#999999"))
  260. }.padding(.horizontal)
  261. }
  262. }
  263. }
  264. struct PurchaseItemView: View {
  265. var title: String
  266. var type: PremiumPeriod
  267. @Binding var selectedType: PremiumPeriod
  268. var body: some View {
  269. ZStack {
  270. Color.white.opacity(0.1)
  271. HStack {
  272. VStack(alignment: .leading, spacing: 8) {
  273. Text(title).font(.font(size: 14)).foregroundColor(UIColor.textAssist.color)
  274. Text(PurchaseManager.default.price(for: type) ?? "--").font(.font(size: 18,weight: .medium)).foregroundColor(UIColor.mainText.color)
  275. }
  276. Spacer()
  277. if type == selectedType {
  278. Image(.radioboxSelected)
  279. }
  280. }.padding(.horizontal)
  281. }
  282. .frame(height: 74) // 设置高度
  283. .cornerRadius(16.0) // 圆角
  284. .overlay(
  285. RoundedRectangle(cornerRadius: 16)
  286. .stroke(Color.hex("#FECB34"), lineWidth: type == selectedType ? 1 : 0) // 边框
  287. )
  288. }
  289. }
  290. //推荐选择view
  291. struct TSVipRecView: View {
  292. var body: some View {
  293. let corner = 16.0
  294. HStack(spacing: 4) {
  295. Image("upvote_black").resizable().frame(width: 16, height: 16)
  296. Text("80% Choose").font(.font(size: 12,weight: .medium)).foregroundColor(.hex("#111111"))
  297. }
  298. .padding(EdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6))
  299. .background(Color.hex("#FECB34"))
  300. .frame(height: 28) // 设置高度
  301. .cornerRadius([.topLeading, .topTrailing, .bottomLeading], 16.0)
  302. }
  303. }