Skip to content

Commit

Permalink
Provide PurchaseManager subscription to settings. Add logic for premi…
Browse files Browse the repository at this point in the history
…um cells in settings.
  • Loading branch information
ant013 committed Dec 27, 2024
1 parent c784fec commit d1f5eee
Show file tree
Hide file tree
Showing 16 changed files with 536 additions and 106 deletions.
20 changes: 20 additions & 0 deletions UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1840,6 +1840,10 @@
6B7EB8262D0D11E900783F66 /* RoundedCorner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B7EB8252D0D11E900783F66 /* RoundedCorner.swift */; };
6B8BD39E2C11B959003ADE10 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B8BD39D2C11B959003ADE10 /* TextFieldAlert.swift */; };
6B8BD39F2C11B959003ADE10 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B8BD39D2C11B959003ADE10 /* TextFieldAlert.swift */; };
6B9032C82D1D6FD400F3F5AC /* PurchaseListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B9032C72D1D6FD400F3F5AC /* PurchaseListView.swift */; };
6B9032C92D1D6FD400F3F5AC /* PurchaseListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B9032C72D1D6FD400F3F5AC /* PurchaseListView.swift */; };
6B9032CB2D1D72F300F3F5AC /* PurchaseListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B9032CA2D1D72F300F3F5AC /* PurchaseListViewModel.swift */; };
6B9032CC2D1D72F300F3F5AC /* PurchaseListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B9032CA2D1D72F300F3F5AC /* PurchaseListViewModel.swift */; };
6BA5117D2BCFA06F00CB5A54 /* FirstAppearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BA5117C2BCFA06F00CB5A54 /* FirstAppearModifier.swift */; };
6BA5117E2BCFA06F00CB5A54 /* FirstAppearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BA5117C2BCFA06F00CB5A54 /* FirstAppearModifier.swift */; };
6BAAF3472B9B245C00EFE5B2 /* ShimmerEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BAAF3442B9B245C00EFE5B2 /* ShimmerEffect.swift */; };
Expand Down Expand Up @@ -4164,6 +4168,8 @@
6B7EB81F2D0C164C00783F66 /* PurchaseSegmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseSegmentView.swift; sourceTree = "<group>"; };
6B7EB8252D0D11E900783F66 /* RoundedCorner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCorner.swift; sourceTree = "<group>"; };
6B8BD39D2C11B959003ADE10 /* TextFieldAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldAlert.swift; sourceTree = "<group>"; };
6B9032C72D1D6FD400F3F5AC /* PurchaseListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseListView.swift; sourceTree = "<group>"; };
6B9032CA2D1D72F300F3F5AC /* PurchaseListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseListViewModel.swift; sourceTree = "<group>"; };
6BA5117C2BCFA06F00CB5A54 /* FirstAppearModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstAppearModifier.swift; sourceTree = "<group>"; };
6BAAF3442B9B245C00EFE5B2 /* ShimmerEffect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShimmerEffect.swift; sourceTree = "<group>"; };
6BAAF3452B9B245C00EFE5B2 /* SlideButtonStyling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SlideButtonStyling.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -7388,6 +7394,15 @@
path = SuccessfulSubscription;
sourceTree = "<group>";
};
6B9032C62D1D6FB900F3F5AC /* PurchaseList */ = {
isa = PBXGroup;
children = (
6B9032C72D1D6FD400F3F5AC /* PurchaseListView.swift */,
6B9032CA2D1D72F300F3F5AC /* PurchaseListViewModel.swift */,
);
path = PurchaseList;
sourceTree = "<group>";
};
6BB14F782C05FAA600E879B2 /* Tvl */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -8861,6 +8876,7 @@
D3E1E8722C21856000E07052 /* Purchases */ = {
isa = PBXGroup;
children = (
6B9032C62D1D6FB900F3F5AC /* PurchaseList */,
6B67F0F02D1C06D8003452DC /* SuccessfulSubscription */,
6BBB92B52D11644D00112409 /* PromoCodeBottomSheet */,
6BBB92AB2D0E889C00112409 /* PurchaseBottomSheet */,
Expand Down Expand Up @@ -9345,6 +9361,7 @@
11B35F66D2561CD9555C8857 /* UnlinkModule.swift in Sources */,
11B35B99C84075296D6F26DE /* UnlinkViewModel.swift in Sources */,
11B35177B650540BCEA880B3 /* UnlinkService.swift in Sources */,
6B9032CC2D1D72F300F3F5AC /* PurchaseListViewModel.swift in Sources */,
1A5645335BEED7A26D53A6B9 /* AddressUri.swift in Sources */,
6B146A982A52A69400648C10 /* ChartIndicatorSettingsViewController.swift in Sources */,
1A564975B127F4EA2FCF61EB /* BitcoinBaseAdapter.swift in Sources */,
Expand Down Expand Up @@ -10408,6 +10425,7 @@
11B357A607396E857705024F /* WalletTokenCell.swift in Sources */,
11B3540028CD7F96521D674F /* WalletTokenListService.swift in Sources */,
11B359FBC96E5ED356519001 /* WalletTokenListViewModel.swift in Sources */,
6B9032C92D1D6FD400F3F5AC /* PurchaseListView.swift in Sources */,
11B354A1F79EDA1E58E50418 /* WalletTokenListViewController.swift in Sources */,
11B35D09F6CBE8A070C415F7 /* WalletTokenListViewItemFactory.swift in Sources */,
11B35208D0EACC4FEAB69B73 /* CexDepositViewItemFactory.swift in Sources */,
Expand Down Expand Up @@ -10824,6 +10842,7 @@
11B3555CA9B2F01358E055BE /* UnlinkViewModel.swift in Sources */,
11B35FB3A17F76325C98C2AB /* UnlinkService.swift in Sources */,
1A5648D075682B17EFE9CBB6 /* AddressUri.swift in Sources */,
6B9032CB2D1D72F300F3F5AC /* PurchaseListViewModel.swift in Sources */,
1A564D2917CF5C38C84C031B /* BitcoinBaseAdapter.swift in Sources */,
1A564C11B7785DCEA2472065 /* BitcoinCashAdapter.swift in Sources */,
6B146A972A52A69400648C10 /* ChartIndicatorSettingsViewController.swift in Sources */,
Expand Down Expand Up @@ -11887,6 +11906,7 @@
11B3565617F5E45C0B86AFED /* ReceiveViewModel.swift in Sources */,
11B351FF1C61A672DB318DC0 /* ReceiveViewController.swift in Sources */,
11B35BB8900C2EE48F99AEFE /* WalletTokenCell.swift in Sources */,
6B9032C82D1D6FD400F3F5AC /* PurchaseListView.swift in Sources */,
11B352EA1BB54971DB960DDF /* WalletTokenListService.swift in Sources */,
11B356A2666F52C272B4E465 /* WalletTokenListViewModel.swift in Sources */,
11B354CF393A2EAFDABE1C47 /* WalletTokenListViewController.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ class PurchaseManager: NSObject {
@PostPublished private(set) var products: [Product] = []
@PostPublished private(set) var purchasedProductIds = Set<String>()

@PostPublished private(set) var subscription: Subscription? //STUB

private var updatesTask: Task<Void, Never>?

override init() {
super.init()

loadProducts()
loadPurchases()
loadSubscription() // STUB
observeTransactionUpdates()

SKPaymentQueue.default().add(self)
Expand Down Expand Up @@ -101,10 +104,6 @@ extension PurchaseManager {
print("UNKNOWN")
}
}

func purchase(period: String) async throws { // STUB
try await Task.sleep(for: .seconds(2))
}
}

extension PurchaseManager: SKPaymentTransactionObserver {
Expand All @@ -118,7 +117,12 @@ extension PurchaseManager: SKPaymentTransactionObserver {
}

extension PurchaseManager {
func check(promocode: String) async throws -> PromoData { // STUB
// STUB BLOCK
private static let subscriptionTypeKey = "subscription_type"
private static let subscriptionPeriodKey = "subscription_period"
private static let subscriptionTimeKey = "subscription_time"

func check(promocode: String) async throws -> PromoData {
if promocode == "" {
return .empty
}
Expand All @@ -131,9 +135,65 @@ extension PurchaseManager {
throw PromoCodeError.invalid
}
}

func purchase(type: SubscriptionType, period: SubscriptionPeriod) async throws {
let storage = App.shared.userDefaultsStorage
let current = Date().timeIntervalSince1970
storage.set(value: type.rawValue, for: Self.subscriptionTypeKey)
storage.set(value: period.rawValue, for: Self.subscriptionPeriodKey)
storage.set(value: current.description, for: Self.subscriptionTimeKey)

subscription = Subscription(type: type, period: period, timestamp: current)
try await Task.sleep(for: .seconds(2))
}

func deactivate() {
let storage = App.shared.userDefaultsStorage
storage.set(value: String?._createNil, for: Self.subscriptionTypeKey)
storage.set(value: String?._createNil, for: Self.subscriptionPeriodKey)
storage.set(value: String?._createNil, for: Self.subscriptionTimeKey)

subscription = nil
}

func loadSubscription() {
let storage = UserDefaultsStorage()
if let subscriptionType: String = storage.value(for: Self.subscriptionTypeKey),
let subscriptionPeriod: String = storage.value(for: Self.subscriptionPeriodKey),
let subscriptionTimeString: String = storage.value(for: Self.subscriptionTimeKey),
let featureType = SubscriptionType(rawValue: subscriptionType),
let featurePeriod = SubscriptionPeriod(rawValue: subscriptionPeriod),
let subscriptionTime = TimeInterval(subscriptionTimeString) {

subscription = Subscription(type: featureType, period: featurePeriod, timestamp: subscriptionTime)
} else {
subscription = nil
}
}
}

extension PurchaseManager {
// STUB BLOCK
enum SubscriptionType: String, CaseIterable {
case pro
case vip
}

enum SubscriptionPeriod: String, CaseIterable {
case annually
case monthly
}

struct Subscription: Identifiable {
let type: SubscriptionType
let period: SubscriptionPeriod
let timestamp: TimeInterval

var id: String {
[type.rawValue, period.rawValue].joined(separator: "|")
}
}

enum PromoCodeError: Error {
case invalid
case used
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,19 @@ struct PurchaseBottomSheetView: View {
@Binding private var isPresented: Bool
@State private var isPresentedPromoCode: Bool = false

private let type: PurchasesViewModel.FeaturesType
private let onSubscribe: (PurchaseBottomSheetViewModel.Period) -> ()
private let onSubscribe: (PurchaseManager.SubscriptionPeriod) -> ()

init(type: PurchasesViewModel.FeaturesType, isPresented: Binding<Bool>, onSubscribe: @escaping (PurchaseBottomSheetViewModel.Period) -> ()) {
_viewModel = StateObject(wrappedValue: PurchaseBottomSheetViewModel(onSubscribe: onSubscribe))
self.type = type
init(type: PurchaseManager.SubscriptionType, isPresented: Binding<Bool>, onSubscribe: @escaping (PurchaseManager.SubscriptionPeriod) -> ()) {
_viewModel = StateObject(wrappedValue: PurchaseBottomSheetViewModel(type: type, onSubscribe: onSubscribe))
self.onSubscribe = onSubscribe
_isPresented = isPresented
}

var body: some View {
VStack(spacing: 0) {
HStack(spacing: .margin16) {
Image(type.icon).themeIcon(color: .themeJacob)
Text(type.rawValue.uppercased()).themeHeadline2()
Image(viewModel.type.icon).themeIcon(color: .themeJacob)
Text(viewModel.type.rawValue.uppercased()).themeHeadline2()

Button(action: {
isPresented = false
Expand All @@ -34,7 +32,7 @@ struct PurchaseBottomSheetView: View {
.padding(.top, .margin24)
.padding(.bottom, .margin12)

SubscribePeriodSegmentView(selection: $viewModel.selectedPeriod)
SubscribePeriodSegmentView(type: viewModel.type, selection: $viewModel.selectedPeriod)
.onChange(of: viewModel.selectedPeriod) { newValue in
viewModel.set(period: newValue)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ import Combine
import Foundation

class PurchaseBottomSheetViewModel: ObservableObject {
@Published var selectedPeriod: Period = .annually
@Published var selectedPeriod: PurchaseManager.SubscriptionPeriod = .annually
@Published var promoData: PurchaseManager.PromoData = .empty
@Published var buttonState: ButtonState = .idle

private let purchaseManager = App.shared.purchaseManager
private let onSubscribe: ((Period) -> ())

let type: PurchaseManager.SubscriptionType
private let onSubscribe: ((PurchaseManager.SubscriptionPeriod) -> ())

init(onSubscribe: @escaping ((Period) -> ())) {
init(type: PurchaseManager.SubscriptionType, onSubscribe: @escaping ((PurchaseManager.SubscriptionPeriod) -> ())) {
self.onSubscribe = onSubscribe
self.type = type
}

@MainActor private func update(state: ButtonState) async {
Expand All @@ -24,7 +27,7 @@ class PurchaseBottomSheetViewModel: ObservableObject {
await update(state: .loading)

do {
try await purchaseManager.purchase(period: selectedPeriod.rawValue)
try await purchaseManager.purchase(type: type, period: selectedPeriod)
await update(state: .idle)
onSubscribe(selectedPeriod)
} catch {
Expand All @@ -34,7 +37,7 @@ class PurchaseBottomSheetViewModel: ObservableObject {
}
}

func set(period: Period) {
func set(period: PurchaseManager.SubscriptionPeriod) {
selectedPeriod = period
}

Expand All @@ -49,55 +52,13 @@ extension PurchaseBottomSheetViewModel {
case loading
}

enum Period: String, CaseIterable, Identifiable {
case annually
case monthly

var title: String {
switch self {
case .annually: return "purchase.period.annually".localized
case .monthly: return "purchase.period.monthly".localized
}
}

var discount: Int? {
switch self {
case .annually: return 45
case .monthly: return nil
}
}

var price: Decimal {
switch self {
case .annually: return 199
case .monthly: return 24
}
}

var pricePeriod: String {
switch self {
case .annually: return "purchase.period.year".localized
case .monthly: return "purchase.period.month".localized
}
}

var unitPrice: Decimal? {
switch self {
case .annually: return (price / 12).rounded(decimal: 2)
case .monthly: return nil
}
}

var id: String { rawValue }
}

struct ViewItem: Hashable {
let title: String
let discountBadge: String?
let price: String
let priceDescription: String?

init(period: Period) {
init(type: PurchaseManager.SubscriptionType, period: PurchaseManager.SubscriptionPeriod) {
self.title = period.title
if let discount = period.discount {
self.discountBadge = ["purchase.period.save".localized.uppercased(), "\(discount)%"].joined(separator: " ")
Expand All @@ -106,8 +67,8 @@ extension PurchaseBottomSheetViewModel {
}


self.price = ["US$\(String(describing: period.price))", " / ", period.pricePeriod].joined(separator: " ")
if let unitPrice = period.unitPrice {
self.price = ["US$\(String(describing: period.price(type: type)))", " / ", period.pricePeriod].joined(separator: " ")
if let unitPrice = period.unitPrice(type: type) {
let price = ["$\(unitPrice)", " / ", "purchase.period.month".localized].joined(separator: " ")
self.priceDescription = "(\(price))"
} else {
Expand All @@ -116,3 +77,42 @@ extension PurchaseBottomSheetViewModel {
}
}
}

extension PurchaseManager.SubscriptionPeriod: Identifiable {
var title: String {
switch self {
case .annually: return "purchase.period.annually".localized
case .monthly: return "purchase.period.monthly".localized
}
}

var discount: Int? {
switch self {
case .annually: return 45
case .monthly: return nil
}
}

func price(type: PurchaseManager.SubscriptionType) -> Decimal {
switch self {
case .annually: return type == .pro ? 199 : 660
case .monthly: return type == .pro ? 24 : 80
}
}

var pricePeriod: String {
switch self {
case .annually: return "purchase.period.year".localized
case .monthly: return "purchase.period.month".localized
}
}

func unitPrice(type: PurchaseManager.SubscriptionType) -> Decimal? {
switch self {
case .annually: return (price(type: type) / 12).rounded(decimal: 2)
case .monthly: return nil
}
}

var id: String { rawValue }
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import SwiftUI

struct SubscribePeriodSegmentView: View {
@Binding var selection: PurchaseBottomSheetViewModel.Period
let type: PurchaseManager.SubscriptionType

@Binding var selection: PurchaseManager.SubscriptionPeriod

var body: some View {
VStack(spacing: 12) {
ForEach(PurchaseBottomSheetViewModel.Period.allCases, id: \.self) { period in
let viewItem = PurchaseBottomSheetViewModel.ViewItem(period: period)
ForEach(PurchaseManager.SubscriptionPeriod.allCases, id: \.self) { period in
let viewItem = PurchaseBottomSheetViewModel.ViewItem(type: type, period: period)

segmentButton(
title: viewItem.title,
Expand Down
Loading

0 comments on commit d1f5eee

Please sign in to comment.