TSPurchaseVC.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  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. func createImageScroll(imageName:String,direction:ImagesAnimateScrollView.`Direction`)->ImagesAnimateScrollView{
  31. let imageScroll1: ImagesAnimateScrollView = ImagesAnimateScrollView()
  32. imageScroll1.direction = direction
  33. imageScroll1.animationImageName = imageName
  34. imageScroll1.transform = CGAffineTransform(rotationAngle: CGFloat.pi/12)
  35. return imageScroll1
  36. }
  37. lazy var bgView: UIView = {
  38. let bgView = UIView()
  39. bgView.backgroundColor = .clear
  40. let imageScroll1 = createImageScroll(imageName: "img-premium-photos-1", direction: .topToBottom)
  41. let imageScroll2 = createImageScroll(imageName: "img-premium-photos-2", direction: .bottomToTop)
  42. let imageScroll3 = createImageScroll(imageName: "img-premium-photos-3", direction: .topToBottom)
  43. let imageScroll4 = createImageScroll(imageName: "img-premium-photos-4", direction: .bottomToTop)
  44. let imageScroll5 = createImageScroll(imageName: "img-premium-photos-5", direction: .topToBottom)
  45. bgView.addSubview(imageScroll1)
  46. bgView.addSubview(imageScroll2)
  47. bgView.addSubview(imageScroll3)
  48. bgView.addSubview(imageScroll4)
  49. bgView.addSubview(imageScroll5)
  50. let top = -40.0
  51. let w = 110.0
  52. let h = 600//554.0
  53. //中间
  54. imageScroll3.snp.makeConstraints { make in
  55. make.top.equalTo(top)
  56. make.centerX.equalToSuperview()
  57. make.width.equalTo(w)
  58. make.height.equalTo(h)
  59. }
  60. //左边
  61. imageScroll2.snp.makeConstraints { make in
  62. make.top.equalTo(top)
  63. make.trailing.equalTo(imageScroll3.snp.leading).offset(-12)
  64. make.width.equalTo(w)
  65. make.height.equalTo(h)
  66. }
  67. imageScroll1.snp.makeConstraints { make in
  68. make.top.equalTo(top)
  69. make.trailing.equalTo(imageScroll2.snp.leading).offset(-12)
  70. make.width.equalTo(w)
  71. make.height.equalTo(h)
  72. }
  73. //右边
  74. imageScroll4.snp.makeConstraints { make in
  75. make.top.equalTo(top)
  76. make.leading.equalTo(imageScroll3.snp.trailing).offset(12)
  77. make.width.equalTo(w)
  78. make.height.equalTo(h)
  79. }
  80. imageScroll5.snp.makeConstraints { make in
  81. make.top.equalTo(top)
  82. make.leading.equalTo(imageScroll4.snp.trailing).offset(12)
  83. make.width.equalTo(w)
  84. make.height.equalTo(h)
  85. }
  86. kDelayMainShort {//0.1 秒后开启动画
  87. imageScroll1.startAnimation()
  88. imageScroll2.startAnimation()
  89. imageScroll3.startAnimation()
  90. imageScroll4.startAnimation()
  91. imageScroll5.startAnimation()
  92. }
  93. let imageView = UIImageView.createImageView(imageName: "purchase_bj",contentMode: .scaleAspectFill)
  94. bgView.addSubview(imageView)
  95. imageView.snp.makeConstraints { make in
  96. make.edges.equalToSuperview()
  97. }
  98. return bgView
  99. }()
  100. lazy var hostVc: UIHostingController<PurchaseView> = {
  101. let vc = UIHostingController(rootView: PurchaseView(viewModel: viewModel))
  102. vc.view.backgroundColor = .clear
  103. return vc
  104. }()
  105. override func createView() {
  106. addNormalNavBarView()
  107. _ = setNavigationItem("", imageName: "close_gray", direction: .left, action: #selector(closePage))
  108. view.insertSubview(bgView, at: 0)
  109. bgView.snp.makeConstraints { make in
  110. make.leading.trailing.bottom.top.equalToSuperview()
  111. }
  112. // setViewBgImageNamed(named: "purchase_bj")
  113. contentView.addSubview(hostVc.view)
  114. hostVc.view.snp.makeConstraints { make in
  115. make.leading.trailing.bottom.top.equalToSuperview()
  116. }
  117. }
  118. override func dealThings() {
  119. addNotifaction()
  120. onPurchaseStateChanged()
  121. }
  122. func addNotifaction() {
  123. viewModel.buyPublisher.receive(on: DispatchQueue.main).sink { [weak self] _ in
  124. guard let self = self else {
  125. return
  126. }
  127. PurchaseManager.default.pay(for: self.viewModel.selectedType)
  128. }.store(in: &cancellabel)
  129. viewModel.privacyPublisher.receive(on: DispatchQueue.main).sink { [weak self] _ in
  130. guard let self = self else {
  131. return
  132. }
  133. let vc = TSBusinessWebVC(urlType: .privacy)
  134. vc.hidesBottomBarWhenPushed = true
  135. kPresentModalVC(target: self, modelVC: vc)
  136. }.store(in: &cancellabel)
  137. viewModel.termPublisher.receive(on: DispatchQueue.main).sink { [weak self] _ in
  138. guard let self = self else {
  139. return
  140. }
  141. let vc = TSBusinessWebVC(urlType: .terms)
  142. vc.hidesBottomBarWhenPushed = true
  143. kPresentModalVC(target: self, modelVC: vc)
  144. }.store(in: &cancellabel)
  145. viewModel.restorePublisher.receive(on: DispatchQueue.main).sink { _ in
  146. PurchaseManager.default.restorePremium()
  147. }.store(in: &cancellabel)
  148. }
  149. func onPurchaseStateChanged(){
  150. purchaseManager.onPurchaseStateChanged = { [weak self] manager,state,object in
  151. guard let self = self else { return }
  152. DispatchQueue.main.async {
  153. switch state {
  154. case .none:
  155. break
  156. case .loading:
  157. TSToastShared.showLoading(text: "Getting price".localized,containerView: self.view)
  158. case .loadSuccess:
  159. TSToastShared.hideLoading()
  160. case .loadFail:
  161. TSToastShared.hideLoading()
  162. let message = "Failed to get the price, will automatically retry in 5 seconds".localized
  163. TSToastShared.showToast(text: message)
  164. DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
  165. PurchaseManager.default.requestProducts()
  166. }
  167. case .paying:
  168. TSToastShared.showLoading(text: "Purchasing now".localized,containerView: self.view)
  169. case .paySuccess:
  170. TSToastShared.hideLoading()
  171. let loadingText = manager.isVip ? "Congratulation you have become VIP".localized : "Finish".localized
  172. TSToastShared.showToast(text:loadingText)
  173. if manager.isVip {
  174. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  175. self.closePage()
  176. }
  177. }
  178. case .payFail:
  179. TSToastShared.hideLoading()
  180. if let str = object as? String {
  181. TSToastShared.showToast(text: str)
  182. }
  183. case .restoreing:
  184. TSToastShared.showLoading(text: "Restoring now".localized,containerView: self.view)
  185. case .restoreSuccess:
  186. TSToastShared.hideLoading()
  187. let loadingText = manager.isVip ? "Congratulation you have become VIP".localized : "Couldn't Restore Subscription".localized
  188. debugPrint(loadingText)
  189. TSToastShared.showToast(text:loadingText)
  190. if manager.isVip {
  191. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  192. self.closePage()
  193. }
  194. }
  195. case .restoreFail:
  196. TSToastShared.hideLoading()
  197. let loadingText = (object as? String) ?? "Failed to restore subscribe, please try again".localized
  198. debugPrint(loadingText)
  199. TSToastShared.showToast(text: loadingText)
  200. case .verifying:
  201. #if DEBUG
  202. TSToastShared.showLoading(text: "Verifying receipt...".localized,containerView: self.view)
  203. #endif
  204. case .verifySuccess:
  205. break
  206. case .verifyFail:
  207. #if DEBUG
  208. TSToastShared.hideLoading()
  209. let message = (object as? String) ?? "Failed to validate receipt".localized
  210. TSToastShared.showToast(text:message)
  211. #endif
  212. }
  213. }
  214. debugPrint("PurchaseManager onPurchaseStateChanged=\(String(describing: state))")
  215. }
  216. }
  217. @objc func closePage(){
  218. closePageBlock?()
  219. TSToastShared.hideLoading()
  220. self.dismiss(animated: true)
  221. }
  222. deinit {
  223. cancellabel.removeAll()
  224. }
  225. }
  226. func kJudgeVip(externalBool:Bool,
  227. vc:UIViewController,
  228. closePageBlock:(()->Void)?) -> Bool {
  229. //判断 vip
  230. if externalBool,
  231. PurchaseManager.default.isVip == false
  232. {
  233. TSPurchaseVC.show(target: vc, closePageBlock: nil)
  234. return true
  235. }
  236. return false
  237. }
  238. extension TSPurchaseVC{
  239. static func show(target:UIViewController,closePageBlock:(()->Void)?){
  240. let vc = TSPurchaseVC()
  241. vc.closePageBlock = closePageBlock
  242. let navi = TSBaseNavigationC(rootViewController: vc)
  243. navi.modalPresentationStyle = .overFullScreen
  244. target.present(navi, animated: true)
  245. }
  246. }
  247. struct PurchaseView :View {
  248. @ObservedObject var viewModel: PurchaseViewModel
  249. var body: some View {
  250. ScrollView {
  251. Spacer().frame(height: 31)
  252. VStack {
  253. customText(text: "\(kAppName) ",fontName: .KelsiFill,color:UIColor.themeColor.color).frame(height: 53*kDesignScale)
  254. Spacer().frame(height: 16)
  255. Text("Premium".localized)
  256. .font(.font(size: 16))
  257. .padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20))
  258. .frame(height: 34)
  259. .foregroundColor(.white)
  260. .background(.hex("#222222"))
  261. .cornerRadius(17.0)
  262. .overlay(
  263. RoundedRectangle(cornerRadius: 16)
  264. .stroke(Color.hex("#FA794F"), lineWidth: 2) // 边框
  265. )
  266. Spacer().frame(height: 36)
  267. ZStack {
  268. VStack(alignment: .leading,spacing: 16) {
  269. HStack(spacing: 12) {
  270. Image("vip_feature_1").resizable().frame(width: 24, height: 24)
  271. Text("Unlimited AI image generation".localized)
  272. }
  273. HStack(spacing: 12) {
  274. Image("vip_feature_2").resizable().frame(width: 24, height: 24)
  275. Text("Change image styles".localized)
  276. }
  277. HStack(spacing: 12) {
  278. Image("vip_feature_3").resizable().frame(width: 24, height: 24)
  279. Text("Unlimited AI chat".localized).multilineTextAlignment(.leading)
  280. }
  281. HStack(spacing: 12) {
  282. Image("vip_feature_4").resizable().frame(width: 24, height: 24)
  283. Text("Ad-free experience".localized).multilineTextAlignment(.leading)
  284. }
  285. }.font(.font(size: 16)).foregroundColor(UIColor.white.color)
  286. }
  287. }
  288. Spacer().frame(height: 44)
  289. VStack(spacing: 12) {
  290. ZStack(alignment: .topTrailing) {
  291. PurchaseItemView(title: "One Year".localized, type: .year, selectedType: $viewModel.selectedType).onTapGesture {
  292. viewModel.selectedType = .year
  293. }
  294. TSVipRecView()
  295. .offset(y:-14)
  296. }
  297. HStack {
  298. PurchaseItemView(title: "One Month".localized, type: .month, selectedType: $viewModel.selectedType).onTapGesture {
  299. viewModel.selectedType = .month
  300. }
  301. PurchaseItemView(title: "One Week".localized, type: .week, selectedType: $viewModel.selectedType).onTapGesture {
  302. viewModel.selectedType = .week
  303. }
  304. }
  305. Button {
  306. viewModel.buyPublisher.send(true)
  307. } label: {
  308. ZStack {
  309. Color.hex("#FFBD59")
  310. Text("Continue")
  311. .font(.system(size: 16))
  312. .foregroundColor(.hex("#111111"))
  313. }.frame(maxWidth: .infinity ,minHeight: 48.0)
  314. .cornerRadius(24.0)
  315. }
  316. HStack {
  317. Text("Recurring billing, cancel anytime".localized)
  318. .foregroundColor(Color.hex("#FFBD59")) +
  319. 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)
  320. .foregroundColor(UIColor.lesserText.color)
  321. }
  322. .multilineTextAlignment(.center).font(.font(size: 8))
  323. .onTapGesture {
  324. viewModel.privacyPublisher.send(true)
  325. }
  326. Spacer().frame(height: 6.0)
  327. HStack(spacing: 8) {
  328. Text("Terms of us".localized)
  329. .onTapGesture {
  330. viewModel.termPublisher.send(true)
  331. }
  332. Text("|")
  333. Text("Privacy Policy".localized)
  334. .onTapGesture {
  335. viewModel.privacyPublisher.send(true)
  336. }
  337. Text("|")
  338. Text("Restore".localized)
  339. .onTapGesture {
  340. viewModel.restorePublisher.send(true)
  341. }
  342. }.font(.system(size: 12)).foregroundColor(.hex("#999999"))
  343. }.padding(.horizontal)
  344. }
  345. }
  346. // 定义一个返回 View 的方法
  347. func customText(text:String,fontName:FontName,color:Color) -> some View {
  348. let gorgeousColor = color //UIColor.themeColor.color
  349. return Text(text)
  350. .font(.font(name: fontName,size: 48))
  351. .gradientForeground(
  352. colors: [.hex("#FA794F"),.hex("#F8C32A"),.hex("#FEFBF4")],
  353. startPoint: UnitPoint.leading,
  354. endPoint: UnitPoint.trailing
  355. )
  356. // .shadow(color: gorgeousColor.opacity(0.7), radius: 6, x: 0, y: 0)
  357. .foregroundColor(gorgeousColor)
  358. .frame(height: 20)
  359. }
  360. }
  361. struct PurchaseItemView: View {
  362. var title: String
  363. var type: PremiumPeriod
  364. @Binding var selectedType: PremiumPeriod
  365. var body: some View {
  366. ZStack {
  367. Color.white.opacity(0.1)
  368. HStack {
  369. VStack(alignment: .leading, spacing: 8) {
  370. Text(title).font(.font(size: 14)).foregroundColor(UIColor.textAssist.color)
  371. Text(PurchaseManager.default.price(for: type) ?? "--").font(.font(size: 18,weight: .medium)).foregroundColor(UIColor.mainText.color)
  372. }
  373. Spacer()
  374. if type == selectedType {
  375. Image(.radioboxSelected)
  376. }
  377. }.padding(.horizontal)
  378. }
  379. .frame(height: 74) // 设置高度
  380. .cornerRadius(16.0) // 圆角
  381. .overlay(
  382. RoundedRectangle(cornerRadius: 16)
  383. .stroke(Color.hex("#FECB34"), lineWidth: type == selectedType ? 1 : 0) // 边框
  384. )
  385. }
  386. }
  387. //推荐选择view
  388. struct TSVipRecView: View {
  389. var body: some View {
  390. HStack(spacing: 4) {
  391. Image("upvote_black").resizable().frame(width: 16, height: 16)
  392. Text("Save-Vip".localized + "60%").font(.font(size: 12,weight: .medium)).foregroundColor(.hex("#111111"))
  393. }
  394. .padding(EdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6))
  395. .background(Color.hex("#FECB34"))
  396. .frame(height: 28) // 设置高度
  397. .cornerRadius([.topLeading, .topTrailing, .bottomLeading], 16.0)
  398. }
  399. }