diff --git a/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinator.swift b/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinator.swift index ff29d3c420..cd7037f086 100644 --- a/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinator.swift +++ b/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinator.swift @@ -50,9 +50,16 @@ struct MembershipCoordinator: View { func tierSelection(tier: MembershipTier) -> some View { - MembershipTierSelectionView(userMembership: model.userMembership, tierToDisplay: tier) { data in - model.onEmailDataSubmit(data: data) - } + MembershipTierSelectionView( + userMembership: model.userMembership, + tierToDisplay: tier, + showEmailVerification: { data in + model.onEmailDataSubmit(data: data) + }, + onSuccessfulPurchase: { tier in + model.onSuccessfulPurchase(tier: tier) + } + ) .sheet(item: $model.emailVerificationData) { data in EmailVerificationView(data: data) { model.onSuccessfulValidation(data: data) diff --git a/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinatorModel.swift b/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinatorModel.swift index f158d7e5ab..656499ab01 100644 --- a/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinatorModel.swift +++ b/Anytype/Sources/PresentationLayer/Flows/MembersipCoordinator/MembershipCoordinatorModel.swift @@ -25,6 +25,17 @@ final class MembershipCoordinatorModel: ObservableObject { loadTiers() } + func loadTiers() { + Task { + do { + tiers = try await membershipService.getTiers() + showTiersLoadingError = false + } catch { + showTiersLoadingError = true + } + } + } + func onTierSelected(tier: MembershipTier) { switch tier.type { case .custom: @@ -46,24 +57,21 @@ final class MembershipCoordinatorModel: ObservableObject { func onSuccessfulValidation(data: EmailVerificationData) { emailVerificationData = nil + showSuccessScreen(tier: data.tier) + } + + func onSuccessfulPurchase(tier: MembershipTier) { + showSuccessScreen(tier: tier) + } + + private func showSuccessScreen(tier: MembershipTier) { showTier = nil loadTiers() // https://linear.app/anytype/issue/IOS-2434/bottom-sheet-nesting Task { try await Task.sleep(seconds: 0.5) - showSuccess = data.tier - } - } - - func loadTiers() { - Task { - do { - tiers = try await membershipService.getTiers() - showTiersLoadingError = false - } catch { - showTiersLoadingError = true - } + showSuccess = tier } } } diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/MembershipTierSelectionView.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/MembershipTierSelectionView.swift index 5c3b7b0a05..68734fec97 100644 --- a/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/MembershipTierSelectionView.swift +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/MembershipTierSelectionView.swift @@ -8,13 +8,16 @@ struct MembershipTierSelectionView: View { init( userMembership: MembershipStatus, tierToDisplay: MembershipTier, - showEmailVerification: @escaping (EmailVerificationData) -> () + showEmailVerification: @escaping (EmailVerificationData) -> (), + onSuccessfulPurchase: @escaping (MembershipTier) -> () + ) { _model = StateObject( wrappedValue: MembershipTierSelectionViewModel( userMembership: userMembership, tierToDisplay: tierToDisplay, - showEmailVerification: showEmailVerification + showEmailVerification: showEmailVerification, + onSuccessfulPurchase: onSuccessfulPurchase ) ) } @@ -32,18 +35,17 @@ struct MembershipTierSelectionView: View { var sheet: some View { Group { - if model.userMembership.tier?.type == model.tierToDisplay.type { + if model.tierOwned { MembershipOwnerInfoSheetView(membership: model.userMembership) - EmptyView() } else { - switch model.tierToDisplay.id { - case .explorer: + switch model.tierToDisplay.paymentType { + case .email: MembershipEmailSheetView { email, subscribeToNewsletter in try await model.getVerificationEmail(email: email, subscribeToNewsletter: subscribeToNewsletter) } - case .builder, .coCreator: - MembershipNameSheetView(tier: model.tierToDisplay, anyName: model.userMembership.anyName) - case .custom: + case .appStore(let product): + MembershipNameSheetView(tier: model.tierToDisplay, anyName: model.userMembership.anyName, product: product, onSuccessfulPurchase: model.onSuccessfulPurchase) + case .external: EmptyView() // TBD in future updates } } @@ -74,7 +76,8 @@ struct MembershipTierSelectionView: View { anyName: "" ), tierToDisplay: .mockExplorer, - showEmailVerification: { _ in } + showEmailVerification: { _ in }, + onSuccessfulPurchase: { _ in } ) MembershipTierSelectionView( userMembership: MembershipStatus( @@ -85,7 +88,8 @@ struct MembershipTierSelectionView: View { anyName: "" ), tierToDisplay: .mockExplorer, - showEmailVerification: { _ in } + showEmailVerification: { _ in }, + onSuccessfulPurchase: { _ in } ) MembershipTierSelectionView( userMembership: MembershipStatus( @@ -96,7 +100,8 @@ struct MembershipTierSelectionView: View { anyName: "" ), tierToDisplay: .mockBuilder, - showEmailVerification: { _ in } + showEmailVerification: { _ in }, + onSuccessfulPurchase: { _ in } ) MembershipTierSelectionView( userMembership: MembershipStatus( @@ -107,7 +112,8 @@ struct MembershipTierSelectionView: View { anyName: "SonyaBlade" ), tierToDisplay: .mockBuilder, - showEmailVerification: { _ in } + showEmailVerification: { _ in }, + onSuccessfulPurchase: { _ in } ) MembershipTierSelectionView( userMembership: MembershipStatus( @@ -118,7 +124,8 @@ struct MembershipTierSelectionView: View { anyName: "SonyaBlade" ), tierToDisplay: .mockCoCreator, - showEmailVerification: { _ in } + showEmailVerification: { _ in }, + onSuccessfulPurchase: { _ in } ) MembershipTierSelectionView( userMembership: MembershipStatus( @@ -129,7 +136,8 @@ struct MembershipTierSelectionView: View { anyName: "SonyaBlade" ), tierToDisplay: .mockCoCreator, - showEmailVerification: { _ in } + showEmailVerification: { _ in }, + onSuccessfulPurchase: { _ in } ) }.tabViewStyle(.page) } diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/MembershipTierSelectionViewModel.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/MembershipTierSelectionViewModel.swift index 704bf3e326..d3a6a65492 100644 --- a/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/MembershipTierSelectionViewModel.swift +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/MembershipTierSelectionViewModel.swift @@ -8,18 +8,26 @@ final class MembershipTierSelectionViewModel: ObservableObject { let userMembership: MembershipStatus let tierToDisplay: MembershipTier + let onSuccessfulPurchase: (MembershipTier) -> () + @Injected(\.membershipService) private var membershipService: MembershipServiceProtocol private let showEmailVerification: (EmailVerificationData) -> () + var tierOwned: Bool { + userMembership.tier?.type == tierToDisplay.type + } + init( userMembership: MembershipStatus, tierToDisplay: MembershipTier, - showEmailVerification: @escaping (EmailVerificationData) -> () + showEmailVerification: @escaping (EmailVerificationData) -> (), + onSuccessfulPurchase: @escaping (MembershipTier) -> () ) { self.userMembership = userMembership self.tierToDisplay = tierToDisplay self.showEmailVerification = showEmailVerification + self.onSuccessfulPurchase = onSuccessfulPurchase } func getVerificationEmail(email: String, subscribeToNewsletter: Bool) async throws { diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetView.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetView.swift index 0d2c595421..c5bea7080f 100644 --- a/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetView.swift +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetView.swift @@ -1,14 +1,15 @@ import SwiftUI import Services - +import StoreKit + struct MembershipNameSheetView: View { @StateObject private var model: MembershipNameSheetViewModel @State private var name = "" - init(tier: MembershipTier, anyName: String) { + init(tier: MembershipTier, anyName: String, product: Product, onSuccessfulPurchase: @escaping (MembershipTier) -> ()) { _model = StateObject( - wrappedValue: MembershipNameSheetViewModel(tier: tier, anyName: anyName) + wrappedValue: MembershipNameSheetViewModel(tier: tier, anyName: anyName, product: product, onSuccessfulPurchase: onSuccessfulPurchase) ) } @@ -28,14 +29,13 @@ struct MembershipNameSheetView: View { AnytypeText("\(model.tier.paymentType.displayPrice ?? "") ", style: .title, color: .Text.primary) + AnytypeText(model.tier.paymentType.localizedPeriod ?? "", style: .relation1Regular, color: .Text.primary) Spacer.fixedHeight(15) - StandardButton( - Loc.payByCard, + AsyncStandardButton( + text: Loc.payByCard, style: .primaryLarge ) { - // TODO: Pay - // Support Test tiers without name and tiers if name already purchased + try await model.purchase() } - .disabled(!model.state.isValidated) + .disabled(!model.canBuyTier) Spacer.fixedHeight(20) } .padding(.horizontal, 20) @@ -116,10 +116,10 @@ struct MembershipNameSheetView: View { } } -#Preview { - TabView { - MembershipNameSheetView(tier: .mockBuilder, anyName: "") - MembershipNameSheetView(tier: .mockCoCreator, anyName: "SonyaBlade") - MembershipNameSheetView(tier: .mockBuilderTest, anyName: "") - }.tabViewStyle(.page) -} +//#Preview { +// TabView { +// MembershipNameSheetView(tier: .mockBuilder, anyName: "") +// MembershipNameSheetView(tier: .mockCoCreator, anyName: "SonyaBlade") +// MembershipNameSheetView(tier: .mockBuilderTest, anyName: "") +// }.tabViewStyle(.page) +//} diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetViewModel.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetViewModel.swift index 9a4a301857..f11789295f 100644 --- a/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetViewModel.swift +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/NameSheet/MembershipNameSheetViewModel.swift @@ -1,6 +1,7 @@ import Services import SwiftUI import AnytypeCore +import StoreKit enum MembershipNameSheetViewState { @@ -44,6 +45,15 @@ final class MembershipNameSheetViewModel: ObservableObject { } } + var canBuyTier: Bool { + switch anyNameAvailability { + case .notAvailable, .alreadyBought: + true + case .availableForPruchase: + state.isValidated + } + } + var minimumNumberOfCharacters: UInt32 { switch tier.anyName { case .none: @@ -58,10 +68,14 @@ final class MembershipNameSheetViewModel: ObservableObject { private var memberhsipService: MembershipServiceProtocol private var validationTask: Task<(), any Error>? + private let product: Product + private let onSuccessfulPurchase: (MembershipTier) -> () - init(tier: MembershipTier, anyName: String) { + init(tier: MembershipTier, anyName: String, product: Product, onSuccessfulPurchase: @escaping (MembershipTier) -> ()) { self.tier = tier self.anyName = anyName + self.product = product + self.onSuccessfulPurchase = onSuccessfulPurchase } func validateName(name: String) { @@ -90,4 +104,37 @@ final class MembershipNameSheetViewModel: ObservableObject { } } } + + // MARK: - Purchase + + func purchase() async throws { + let result = try await product.purchase(options: [ + .appAccountToken(UUID()), + .custom(key: "TODO", value: "SEND_DATA") + ]) + + switch result { + case .success(let verificationResult): + let transaction = try checkVerified(verificationResult) + // TODO: Update middleware + await transaction.finish() + onSuccessfulPurchase(tier) + case .userCancelled: + break // TODO + case .pending: + break // TODO + @unknown default: + anytypeAssertionFailure("Unsupported purchase result \(result)") + fatalError() + } + } + + private func checkVerified(_ verificationResult: VerificationResult) throws -> T { + switch verificationResult { + case .unverified(_, let verificationError): + throw verificationError + case .verified(let signedType): + return signedType + } + } } diff --git a/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/Views/MembershipOwnerInfoSheetView.swift b/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/Views/MembershipOwnerInfoSheetView.swift index e247153694..89f9861a63 100644 --- a/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/Views/MembershipOwnerInfoSheetView.swift +++ b/Anytype/Sources/PresentationLayer/Modules/Membership/TierSelection/Views/MembershipOwnerInfoSheetView.swift @@ -25,11 +25,11 @@ struct MembershipOwnerInfoSheetView: View { case .explorer: AnytypeText(Loc.forever, style: .title, color: .Text.primary) Spacer.fixedHeight(55) - case .builder, .coCreator: + case .builder, .coCreator, .custom: AnytypeText(membership.formattedDateEnds, style: .title, color: .Text.primary) paymentText - case .custom, .none: - EmptyView() // TBD in future updates + case .none: + EmptyView() } } .frame(maxWidth: .infinity)