From 79fe5b5daf09e92e19bbd23963feb0d62b612434 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Tue, 28 Oct 2025 14:38:16 +1100 Subject: [PATCH 01/60] feat: error and loading state for non pro --- .../DeveloperSettingsProViewModel.swift | 47 +++++++------- .../SessionProSettingsViewModel.swift | 63 +++++++++++++------ .../SessionProPaymentScreen.swift | 3 +- .../Shared/SessionListScreen+ListItem.swift | 2 +- .../SessionListScreen+ListItemView.swift | 26 ++++---- .../Screens/Shared/SessionListScreen.swift | 4 +- 6 files changed, 87 insertions(+), 58 deletions(-) diff --git a/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift b/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift index 6e341e7fdc..713b9dd55e 100644 --- a/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift +++ b/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift @@ -416,33 +416,30 @@ class DeveloperSettingsProViewModel: SessionTableViewModel, NavigatableStateHold ) } ), - ( - state.mockCurrentUserSessionPro == .none ? nil : - SessionCell.Info( - id: .loadingState, - title: "Loading State", - trailingAccessory: .dropDown { state.loadingState.title }, - onTap: { [weak viewModel, dependencies = viewModel.dependencies] in - viewModel?.transitionToScreen( - SessionTableViewController( - viewModel: SessionListViewModel( - title: "Session Pro Loading State", - options: SessionProLoadingState.allCases, - behaviour: .autoDismiss( - initialSelection: state.loadingState, - onOptionSelected: { [dependencies] selected in - dependencies.set( - feature: .mockCurrentUserSessionProLoadingState, - to: selected - ) - } - ), - using: dependencies - ) - ) + SessionCell.Info( + id: .loadingState, + title: "Loading State", + trailingAccessory: .dropDown { state.loadingState.title }, + onTap: { [weak viewModel, dependencies = viewModel.dependencies] in + viewModel?.transitionToScreen( + SessionTableViewController( + viewModel: SessionListViewModel( + title: "Session Pro Loading State", + options: SessionProLoadingState.allCases, + behaviour: .autoDismiss( + initialSelection: state.loadingState, + onOptionSelected: { [dependencies] selected in + dependencies.set( + feature: .mockCurrentUserSessionProLoadingState, + to: selected + ) + } + ), + using: dependencies ) - } + ) ) + } ), SessionCell.Info( id: .allUsersSessionPro, diff --git a/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift b/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift index ec07534382..fd9b5edbf7 100644 --- a/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift +++ b/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift @@ -241,21 +241,12 @@ public class SessionProSettingsViewModel: SessionListScreenContent.ViewModelType } }(), state: { - guard state.currentProPlanState != .none else { - return .success( - description: "proFullestPotential" - .put(key: "app_name", value: Constants.app_name) - .put(key: "app_pro", value: Constants.app_pro) - .localizedFormatted() - ) - } - switch state.loadingState { case .loading: return .loading( message: { switch state.currentProPlanState { - case .expired: + case .expired, .none: "checkingProStatus" .put(key: "pro", value: Constants.pro) .localized() @@ -270,7 +261,7 @@ public class SessionProSettingsViewModel: SessionListScreenContent.ViewModelType return .error( message: { switch state.currentProPlanState { - case .expired: + case .expired, .none: "errorCheckingProStatus" .put(key: "pro", value: Constants.pro) .localized() @@ -282,9 +273,16 @@ public class SessionProSettingsViewModel: SessionListScreenContent.ViewModelType }() ) case .success: - return .success(description: nil) + return .success } - }() + }(), + description: ( + state.currentProPlanState != .none ? nil : + "proFullestPotential" + .put(key: "app_name", value: Constants.app_name) + .put(key: "app_pro", value: Constants.app_pro) + .localizedFormatted() + ) ) ), onTap: { [weak viewModel] in @@ -294,11 +292,11 @@ public class SessionProSettingsViewModel: SessionListScreenContent.ViewModelType from: .logoWithPro, title: { switch state.currentProPlanState { - case .active, .refunding, .none: + case .active, .refunding: "proStatusLoading" .put(key: "pro", value: Constants.pro) .localized() - case .expired: + case .expired, .none: "checkingProStatus" .put(key: "pro", value: Constants.pro) .localized() @@ -306,7 +304,7 @@ public class SessionProSettingsViewModel: SessionListScreenContent.ViewModelType }(), description: { switch state.currentProPlanState { - case .active, .refunding, .none: + case .active, .refunding: "proStatusLoadingDescription" .put(key: "pro", value: Constants.pro) .localized() @@ -314,6 +312,10 @@ public class SessionProSettingsViewModel: SessionListScreenContent.ViewModelType "checkingProStatusDescription" .put(key: "pro", value: Constants.pro) .localized() + case .none: + "checkingProStatusContinue" + .put(key: "pro", value: Constants.pro) + .localized() } }() ) @@ -336,8 +338,33 @@ public class SessionProSettingsViewModel: SessionListScreenContent.ViewModelType state.currentProPlanState != .none ? nil : SessionListScreenContent.ListItemInfo( id: .continueButton, - variant: .button(title: "theContinue".localized()), - onTap: { [weak viewModel] in viewModel?.updateProPlan() } + variant: .button(title: "theContinue".localized(), enabled: (state.loadingState == .success)), + onTap: { [weak viewModel] in + switch state.loadingState { + case .loading: + viewModel?.showLoadingModal( + from: .logoWithPro, + title: "checkingProStatus" + .put(key: "pro", value: Constants.pro) + .localized(), + description: "checkingProStatusContinue" + .put(key: "pro", value: Constants.pro) + .localized() + ) + case .error: + viewModel?.showErrorModal( + from: .logoWithPro, + title: "proStatusError" + .put(key: "pro", value: Constants.pro) + .localized(), + description: "proStatusRefreshNetworkError" + .put(key: "pro", value: Constants.pro) + .localizedFormatted() + ) + case .success: + viewModel?.updateProPlan() + } + } ) ) ].compactMap { $0 } diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift index 1ee014b3bf..60b2677061 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift @@ -41,7 +41,8 @@ public struct SessionProPaymentScreen: View { default: return .normal } }(), - state: .success(description: viewModel.dataModel.flow.description) + state: .success, + description: viewModel.dataModel.flow.description ) ) if case .purchase = viewModel.dataModel.flow { diff --git a/SessionUIKit/Screens/Shared/SessionListScreen+ListItem.swift b/SessionUIKit/Screens/Shared/SessionListScreen+ListItem.swift index 365d7c1cba..f60f897fea 100644 --- a/SessionUIKit/Screens/Shared/SessionListScreen+ListItem.swift +++ b/SessionUIKit/Screens/Shared/SessionListScreen+ListItem.swift @@ -27,7 +27,7 @@ public extension SessionListScreenContent { case cell(info: ListItemCell.Info) case logoWithPro(info: ListItemLogoWithPro.Info) case dataMatrix(info: [[ListItemDataMatrix.Info]]) - case button(title: String) + case button(title: String, enabled: Bool) } let id: ID diff --git a/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift b/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift index a6efe2020e..999af7e4d0 100644 --- a/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift +++ b/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift @@ -137,16 +137,18 @@ public struct ListItemLogoWithPro: View { public enum State: Equatable, Hashable { case loading(message: String) case error(message: String) - case success(description: ThemedAttributedString?) + case success } public struct Info: Equatable, Hashable, Differentiable { public let style: ThemeStyle public let state: State + public let description: ThemedAttributedString? - public init(style: ThemeStyle, state: State) { + public init(style: ThemeStyle, state: State, description: ThemedAttributedString? = nil) { self.style = style self.state = state + self.description = description } } @@ -189,14 +191,6 @@ public struct ListItemLogoWithPro: View { SessionProBadge_SwiftUI(size: .medium, themeBackgroundColor: info.style.themeColor) } - if case .success(let description) = info.state, let description { - AttributedText(description) - .font(.Body.baseRegular) - .foregroundColor(themeColor: .textPrimary) - .multilineTextAlignment(.center) - .padding(.vertical, Values.largeSpacing) - } - if case .error(let message) = info.state { HStack(spacing: Values.verySmallSpacing) { Text(message) @@ -220,6 +214,15 @@ public struct ListItemLogoWithPro: View { .foregroundColor(themeColor: .textPrimary) .padding(.top, Values.mediumSpacing) } + + if let description = info.description { + AttributedText(description) + .font(.Body.baseRegular) + .foregroundColor(themeColor: .textPrimary) + .multilineTextAlignment(.center) + .padding(.top, Values.mediumSpacing) + .padding(.bottom, Values.largeSpacing) + } } .frame(maxWidth: .infinity, alignment: .center) .contentShape(Rectangle()) @@ -341,6 +344,7 @@ public struct ListItemDataMatrix: View { struct ListItemButton: View { let title: String + let enabled: Bool var body: some View { Text(title) @@ -353,7 +357,7 @@ struct ListItemButton: View { ) .background( RoundedRectangle(cornerRadius: 7) - .fill(themeColor: .sessionButton_primaryFilledBackground) + .fill(themeColor: enabled ? .sessionButton_primaryFilledBackground : .disabled) ) .padding(.vertical, Values.smallSpacing) } diff --git a/SessionUIKit/Screens/Shared/SessionListScreen.swift b/SessionUIKit/Screens/Shared/SessionListScreen.swift index 142e96bdfd..71be3a36b8 100644 --- a/SessionUIKit/Screens/Shared/SessionListScreen.swift +++ b/SessionUIKit/Screens/Shared/SessionListScreen.swift @@ -113,8 +113,8 @@ public struct SessionListScreen Date: Wed, 29 Oct 2025 10:39:43 +1100 Subject: [PATCH 02/60] feat: bottom sheet container in SwiftUI --- Session.xcodeproj/project.pbxproj | 4 + .../Components/SwiftUI/BottomSheet.swift | 113 ++++++++++++++++++ .../Utilities/SwiftUI+Utilities.swift | 23 ++++ 3 files changed, 140 insertions(+) create mode 100644 SessionUIKit/Components/SwiftUI/BottomSheet.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 22f18bd29f..c6b591a4ca 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -213,6 +213,7 @@ 947D7FE72D51837200E8E413 /* ArrowCapsule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE42D51837200E8E413 /* ArrowCapsule.swift */; }; 947D7FE82D51837200E8E413 /* PopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE52D51837200E8E413 /* PopoverView.swift */; }; 947D7FE92D51837200E8E413 /* Text+CopyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE62D51837200E8E413 /* Text+CopyButton.swift */; }; + 94805EB22EB087FD0055EBBC /* BottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EB12EB087F90055EBBC /* BottomSheet.swift */; }; 9499E6032DDD9BF900091434 /* ExpandableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */; }; 9499E68B2DF92F4E00091434 /* ThreadNotificationSettingsViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */; }; 94A6B9DB2DD6BF7C00DB4B44 /* Constants+Apple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */; }; @@ -1632,6 +1633,7 @@ 947D7FE42D51837200E8E413 /* ArrowCapsule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrowCapsule.swift; sourceTree = ""; }; 947D7FE52D51837200E8E413 /* PopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverView.swift; sourceTree = ""; }; 947D7FE62D51837200E8E413 /* Text+CopyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Text+CopyButton.swift"; sourceTree = ""; }; + 94805EB12EB087F90055EBBC /* BottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheet.swift; sourceTree = ""; }; 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableLabel.swift; sourceTree = ""; }; 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadNotificationSettingsViewModelSpec.swift; sourceTree = ""; }; 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Constants+Apple.swift"; sourceTree = ""; }; @@ -2932,6 +2934,7 @@ 94B6BAFD2E39F50E00E718BB /* UserProfileModal.swift */, 9438658E2EAB37F600DB989A /* MutipleLinksModal.swift */, 94AAB14A2E1E197800A6FA18 /* Modal+SwiftUI.swift */, + 94805EB12EB087F90055EBBC /* BottomSheet.swift */, FD8A5B1F2DC03332004C689B /* AdaptiveText.swift */, FD8A5B212DC0489B004C689B /* AdaptiveHStack.swift */, 947D7FE42D51837200E8E413 /* ArrowCapsule.swift */, @@ -6430,6 +6433,7 @@ 94363E662E60186A0004EE43 /* SessionListScreen+Section.swift in Sources */, FDB11A612DD5BDCC00BEF49F /* ImageDataManager.swift in Sources */, FD6673FD2D77F54600041530 /* ScreenLockViewController.swift in Sources */, + 94805EB22EB087FD0055EBBC /* BottomSheet.swift in Sources */, 94B6BB042E3B208C00E718BB /* Seperator+SwiftUI.swift in Sources */, FD8A5B222DC0489C004C689B /* AdaptiveHStack.swift in Sources */, FD42ECD02E289261002D03EA /* ThemeLinearGradient.swift in Sources */, diff --git a/SessionUIKit/Components/SwiftUI/BottomSheet.swift b/SessionUIKit/Components/SwiftUI/BottomSheet.swift new file mode 100644 index 0000000000..65a4aeab70 --- /dev/null +++ b/SessionUIKit/Components/SwiftUI/BottomSheet.swift @@ -0,0 +1,113 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import SwiftUI +import Lucide + +public struct BottomSheet: View where Content: View { + let host: HostWrapper + let dismissType: Modal.DismissType + let hasCloseButton: Bool + let afterClosed: (() -> Void)? + let content: (@escaping ((() -> Void)?) -> Void) -> Content + + let cornerRadius: CGFloat = 11 + let shadowRadius: CGFloat = 10 + let shadowOpacity: Double = 0.4 + + @State private var show: Bool = true + + public var body: some View { + ZStack(alignment: .bottom) { + // Background + Rectangle() + .fill(.ultraThinMaterial) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea() + .onTapGesture { close() } + + // Bottom Sheet + VStack { + Spacer() + + ZStack(alignment: .topTrailing) { + content { internalAfterClosed in + close(internalAfterClosed) + } + + if hasCloseButton { + Button { + close(nil) + } label: { + AttributedText(Lucide.Icon.x.attributedString(size: 20)) + .font(.system(size: 20)) + .foregroundColor(themeColor: .textPrimary) + } + .frame(width: 24, height: 24) + .padding(Values.mediumSmallSpacing) + } + } + .backgroundColor(themeColor: .alert_background) + .cornerRadius(cornerRadius, corners: [.topLeft, .topRight]) + .shadow(color: Color.black.opacity(shadowOpacity), radius: shadowRadius) + .frame( + maxWidth: .infinity, + alignment: .topTrailing + ) + } + .transition(.move(edge: .bottom).combined(with: .opacity)) + .animation(.spring(), value: show) + } + .frame( + maxWidth: .infinity, + maxHeight: .infinity, + alignment: .bottom + ) + .gesture( + DragGesture(minimumDistance: 20, coordinateSpace: .global) + .onEnded { value in + if value.translation.height > 40 { + close() + } + } + ) + } + + // MARK: - Dismiss Logic + + private func close(_ internalAfterClosed: (() -> Void)? = nil) { + host.controller?.presentingViewController?.dismiss( + animated: true, + completion: { + afterClosed?() + internalAfterClosed?() + } + ) + } +} + +// MARK: - ModalHostingViewController + +open class BottomSheetHostingViewController: UIHostingController>> where Content: View { + public init(bottomSheet: Content) { + let container = HostWrapper() + let modified = bottomSheet.environmentObject(container) as! ModifiedContent> + super.init(rootView: modified) + container.controller = self + self.modalTransitionStyle = .coverVertical + self.modalPresentationStyle = .overFullScreen + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.backButtonTitle = "" + view.themeBackgroundColor = .clear + ThemeManager.applyNavigationStylingIfNeeded(to: self) + + setNeedsStatusBarAppearanceUpdate() + } +} diff --git a/SessionUIKit/Utilities/SwiftUI+Utilities.swift b/SessionUIKit/Utilities/SwiftUI+Utilities.swift index 43aa9e263a..2b7c930981 100644 --- a/SessionUIKit/Utilities/SwiftUI+Utilities.swift +++ b/SessionUIKit/Utilities/SwiftUI+Utilities.swift @@ -310,3 +310,26 @@ struct HideScrollIndicators: ViewModifier { } } } + +// MARK: Rounded Corner +// FIXME: Remove this and use clipShape(.rect()) when we only support iOS 16+ + +struct RoundedCorner: Shape { + var radius: CGFloat = .infinity + var corners: UIRectCorner = .allCorners + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath( + roundedRect: rect, + byRoundingCorners: corners, + cornerRadii: CGSize(width: radius, height: radius) + ) + return Path(path.cgPath) + } +} + +extension View { + func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { + clipShape(RoundedCorner(radius: radius, corners: corners)) + } +} From 1237456d3fd43ea02e6bc65e875d7cc81e8698d8 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 29 Oct 2025 14:06:06 +1100 Subject: [PATCH 03/60] clean up and refactor --- .../Settings/ThreadSettingsViewModel.swift | 50 ++------ .../MessageInfoScreen.swift | 43 ++++--- .../SessionProSettingsViewModel.swift | 113 +++++++++--------- .../Utilities/SessionProState.swift | 5 +- 4 files changed, 95 insertions(+), 116 deletions(-) diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 73efb45f44..1c80b62196 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -337,19 +337,20 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi return } - let proCTAModalVariant: ProCTAModal.Variant = { - switch threadViewModel.threadVariant { - case .group: - return .groupLimit( + switch threadViewModel.threadVariant { + case .group: + dependencies[singleton: .sessionProState].showSessionProCTAIfNeeded( + .groupLimit( isAdmin: currentUserIsClosedGroupAdmin, isSessionProActivated: (dependencies.mutate(cache: .libSession) { $0.validateSessionProState(for: threadId) }), proBadgeImage: SessionProBadge(size: .mini).toImage(using: dependencies) - ) - default: return .generic - } - }() - - self?.showSessionProCTAIfNeeded(proCTAModalVariant) + ), + presenting: { modal in + self?.transitionToScreen(modal, transitionType: .present) + } + ) + default: break + } } ), @@ -1550,8 +1551,7 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi current: String?, displayName: String ) -> ConfirmationModal.Info { - /// Set `updatedName` to `current` so we can disable the "save" button when there are no changes and don't need to worry - /// about retrieving them in the confirmation closure + /// Set `updatedName` to `current` so we can disable the "save" button when there are no changes and don't need to worry about retrieving them in the confirmation closure self.updatedName = current return ConfirmationModal.Info( title: "nicknameSet".localized(), @@ -2074,32 +2074,6 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi } } - private func showSessionProCTAIfNeeded(_ variant: ProCTAModal.Variant) { - let shouldShowProCTA: Bool = { - guard dependencies[feature: .sessionProEnabled] else { return false } - if case .groupLimit = variant { return true } - return !dependencies[cache: .libSession].isSessionPro - }() - - guard shouldShowProCTA else { return } - - let sessionProModal: ModalHostingViewController = ModalHostingViewController( - modal: ProCTAModal( - variant: variant, - dataManager: dependencies[singleton: .imageDataManager], - onConfirm: { [dependencies] in - dependencies[singleton: .sessionProState].upgradeToPro( - plan: SessionProPlan(variant: .threeMonths), - originatingPlatform: .iOS, - completion: nil - ) - } - ) - ) - - self.transitionToScreen(sessionProModal, transitionType: .present) - } - private func showQRCodeLightBox(for threadViewModel: SessionThreadViewModel) { let qrCodeImage: UIImage = QRCode.generate( for: threadViewModel.getQRCodeString(), diff --git a/Session/Media Viewing & Editing/MessageInfoScreen.swift b/Session/Media Viewing & Editing/MessageInfoScreen.swift index a5915e19c9..b2dcd11e9f 100644 --- a/Session/Media Viewing & Editing/MessageInfoScreen.swift +++ b/Session/Media Viewing & Editing/MessageInfoScreen.swift @@ -306,7 +306,12 @@ struct MessageInfoScreen: View { .foregroundColor(themeColor: .textPrimary) } .onTapGesture { - showSessionProCTAIfNeeded() + dependencies[singleton: .sessionProState].showSessionProCTAIfNeeded( + proCTAVariant, + presenting: { modal in + self.host.controller?.present(modal, animated: true) + } + ) } Text( @@ -395,7 +400,12 @@ struct MessageInfoScreen: View { if (dependencies.mutate(cache: .libSession) { $0.validateSessionProState(for: messageViewModel.authorId)}) { SessionProBadge_SwiftUI(size: .small) .onTapGesture { - showSessionProCTAIfNeeded() + dependencies[singleton: .sessionProState].showSessionProCTAIfNeeded( + proCTAVariant, + presenting: { modal in + self.host.controller?.present(modal, animated: true) + } + ) } } } @@ -535,26 +545,6 @@ struct MessageInfoScreen: View { return (proFeatures, proCTAVariant) } - private func showSessionProCTAIfNeeded() { - guard dependencies[feature: .sessionProEnabled] && (!dependencies[cache: .libSession].isSessionPro) else { - return - } - let sessionProModal: ModalHostingViewController = ModalHostingViewController( - modal: ProCTAModal( - variant: proCTAVariant, - dataManager: dependencies[singleton: .imageDataManager], - onConfirm: { [dependencies] in - dependencies[singleton: .sessionProState].upgradeToPro( - plan: SessionProPlan(variant: .threeMonths), - originatingPlatform: .iOS, - completion: nil - ) - } - ) - ) - self.host.controller?.present(sessionProModal, animated: true) - } - func showUserProfileModal() { guard threadCanWrite else { return } // FIXME: Add in support for starting a thread with a 'blinded25' id (disabled until we support this decoding) @@ -628,7 +618,14 @@ struct MessageInfoScreen: View { isProUser: dependencies.mutate(cache: .libSession, { $0.validateProProof(for: messageViewModel.profile) }), isMessageRequestsEnabled: isMessasgeRequestsEnabled, onStartThread: self.onStartThread, - onProBadgeTapped: self.showSessionProCTAIfNeeded + onProBadgeTapped: { + dependencies[singleton: .sessionProState].showSessionProCTAIfNeeded( + proCTAVariant, + presenting: { modal in + self.host.controller?.present(modal, animated: true) + } + ) + } ), dataManager: dependencies[singleton: .imageDataManager] ) diff --git a/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift b/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift index fd9b5edbf7..9b334f983a 100644 --- a/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift +++ b/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift @@ -16,6 +16,7 @@ public class SessionProSettingsViewModel: SessionListScreenContent.ViewModelType public let navigatableState: NavigatableState = NavigatableState() public let title: String = "" public let state: SessionListScreenContent.ListItemDataState = SessionListScreenContent.ListItemDataState() + private let isBottomSheet: Bool /// This value is the current state of the view @MainActor @Published private(set) var internalState: ViewModelState @@ -24,8 +25,10 @@ public class SessionProSettingsViewModel: SessionListScreenContent.ViewModelType // MARK: - Initialization @MainActor init( + isBottomSheet: Bool = false, using dependencies: Dependencies ) { + self.isBottomSheet = isBottomSheet self.dependencies = dependencies self.internalState = ViewModelState.initialState() @@ -370,6 +373,62 @@ public class SessionProSettingsViewModel: SessionListScreenContent.ViewModelType ].compactMap { $0 } ) + let proFeatures: SectionModel = SectionModel( + model: .proFeatures, + elements: ProFeaturesInfo.allCases(state.currentProPlanState).map { info in + SessionListScreenContent.ListItemInfo( + id: info.id, + variant: .cell( + info: .init( + leadingAccessory: .icon( + info.icon, + iconSize: .medium, + customTint: .black, + gradientBackgroundColors: info.backgroundColors, + backgroundSize: .veryLarge, + backgroundCornerRadius: 8 + ), + title: .init(info.title, font: .Headings.H9, accessory: info.accessory), + description: .init(info.description, font: .Body.smallRegular, color: .textSecondary) + ) + ) + ) + }.appending( + SessionListScreenContent.ListItemInfo( + id: .plusLoadsMore, + variant: .cell( + info: .init( + leadingAccessory: .icon( + Lucide.image(icon: .circlePlus, size: IconSize.medium.size), + iconSize: .medium, + customTint: .black, + gradientBackgroundColors: { + return switch state.currentProPlanState { + case .expired: [ThemeValue.disabled] + default: [.explicitPrimary(.orange), .explicitPrimary(.yellow)] + } + }(), + backgroundSize: .veryLarge, + backgroundCornerRadius: 8 + ), + title: .init("plusLoadsMore".localized(), font: .Headings.H9), + description: .init( + font: .Body.smallRegular, + attributedString: "plusLoadsMoreDescription" + .put(key: "pro", value: Constants.pro) + .put(key: "icon", value: Lucide.Icon.squareArrowUpRight) + .localizedFormatted(Fonts.Body.smallRegular), + color: .textSecondary + ) + ) + ), + onTap: { [weak viewModel] in viewModel?.openUrl(Constants.session_pro_roadmap) } + ) + ) + ) + + guard !viewModel.isBottomSheet else { return [ logo, proFeatures ] } + let proStats: SectionModel = SectionModel( model: .proStats, elements: [ @@ -473,60 +532,6 @@ public class SessionProSettingsViewModel: SessionListScreenContent.ViewModelType elements: getProSettingsElements(state: state, previousState: previousState, viewModel: viewModel) ) - let proFeatures: SectionModel = SectionModel( - model: .proFeatures, - elements: ProFeaturesInfo.allCases(state.currentProPlanState).map { info in - SessionListScreenContent.ListItemInfo( - id: info.id, - variant: .cell( - info: .init( - leadingAccessory: .icon( - info.icon, - iconSize: .medium, - customTint: .black, - gradientBackgroundColors: info.backgroundColors, - backgroundSize: .veryLarge, - backgroundCornerRadius: 8 - ), - title: .init(info.title, font: .Headings.H9, accessory: info.accessory), - description: .init(info.description, font: .Body.smallRegular, color: .textSecondary) - ) - ) - ) - }.appending( - SessionListScreenContent.ListItemInfo( - id: .plusLoadsMore, - variant: .cell( - info: .init( - leadingAccessory: .icon( - Lucide.image(icon: .circlePlus, size: IconSize.medium.size), - iconSize: .medium, - customTint: .black, - gradientBackgroundColors: { - return switch state.currentProPlanState { - case .expired: [ThemeValue.disabled] - default: [.explicitPrimary(.orange), .explicitPrimary(.yellow)] - } - }(), - backgroundSize: .veryLarge, - backgroundCornerRadius: 8 - ), - title: .init("plusLoadsMore".localized(), font: .Headings.H9), - description: .init( - font: .Body.smallRegular, - attributedString: "plusLoadsMoreDescription" - .put(key: "pro", value: Constants.pro) - .put(key: "icon", value: Lucide.Icon.squareArrowUpRight) - .localizedFormatted(Fonts.Body.smallRegular), - color: .textSecondary - ) - ) - ), - onTap: { [weak viewModel] in viewModel?.openUrl(Constants.session_pro_roadmap) } - ) - ) - ) - let proManagement: SectionModel = SectionModel( model: .proManagement, elements: getProManagementElements(state: state, viewModel: viewModel) diff --git a/SessionMessagingKit/Utilities/SessionProState.swift b/SessionMessagingKit/Utilities/SessionProState.swift index f5e5c46d5f..0eece79e9b 100644 --- a/SessionMessagingKit/Utilities/SessionProState.swift +++ b/SessionMessagingKit/Utilities/SessionProState.swift @@ -159,7 +159,10 @@ public class SessionProState: SessionProManagerType, ProfilePictureAnimationMana variant: variant, dataManager: dependencies[singleton: .imageDataManager], dismissType: dismissType, - afterClosed: afterClosed + afterClosed: afterClosed, + onConfirm: { + + } ) ) presenting?(sessionProModal) From 0712148f3ad4c14f924da9145f0b48905c19e291 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 29 Oct 2025 15:00:43 +1100 Subject: [PATCH 04/60] fix: incorrectly now showing the pro cta modal --- Session/Settings/SettingsViewModel.swift | 2 ++ SessionMessagingKit/Utilities/SessionProState.swift | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index c6196cc731..5610bbbadf 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -776,6 +776,8 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl dataManager: dependencies[singleton: .imageDataManager], onProBageTapped: { [weak self, dependencies] in Task { @MainActor in + guard case .active = dependencies[singleton: .sessionProState].sessionProStateSubject.value else { return } + dependencies[singleton: .sessionProState].showSessionProCTAIfNeeded( .animatedProfileImage( isSessionProActivated: dependencies[cache: .libSession].isSessionPro diff --git a/SessionMessagingKit/Utilities/SessionProState.swift b/SessionMessagingKit/Utilities/SessionProState.swift index 0eece79e9b..c09e751cb1 100644 --- a/SessionMessagingKit/Utilities/SessionProState.swift +++ b/SessionMessagingKit/Utilities/SessionProState.swift @@ -150,7 +150,7 @@ public class SessionProState: SessionProManagerType, ProfilePictureAnimationMana afterClosed: (() -> Void)?, presenting: ((UIViewController) -> Void)? ) -> Bool { - guard dependencies[feature: .sessionProEnabled], case .active = sessionProStateSubject.value else { + guard dependencies[feature: .sessionProEnabled] else { return false } beforePresented?() From 6173bfd4ac6897247eeb41d4f2dbd080b470add6 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 30 Oct 2025 17:30:21 +1100 Subject: [PATCH 05/60] WIP: Session Pro bottom sheet --- Session.xcodeproj/project.pbxproj | 20 ++ .../ConversationVC+Interaction.swift | 9 + .../Settings/ThreadSettingsViewModel.swift | 3 + .../MessageInfoScreen.swift | 9 + .../SessionProSettingsViewModel.swift | 257 +++++--------- Session/Settings/SettingsViewModel.swift | 15 +- .../SessionProState+BottomSheet.swift | 318 ++++++++++++++++++ .../Utilities/SessionProState.swift | 33 +- .../Components/SwiftUI/BottomSheet.swift | 31 +- .../Components/SwiftUI/ProCTAModal.swift | 26 +- .../SessionProBottomSheet+Models.swift | 67 ++++ .../SessionProBottomSheet.swift | 26 ++ .../AttachmentApprovalViewController.swift | 6 + 13 files changed, 622 insertions(+), 198 deletions(-) create mode 100644 SessionMessagingKit/Utilities/SessionProState+BottomSheet.swift create mode 100644 SessionUIKit/Screens/Settings/SessionProBottomSheet/SessionProBottomSheet+Models.swift create mode 100644 SessionUIKit/Screens/Settings/SessionProBottomSheet/SessionProBottomSheet.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index c6b591a4ca..a7ae256c1f 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -214,6 +214,9 @@ 947D7FE82D51837200E8E413 /* PopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE52D51837200E8E413 /* PopoverView.swift */; }; 947D7FE92D51837200E8E413 /* Text+CopyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE62D51837200E8E413 /* Text+CopyButton.swift */; }; 94805EB22EB087FD0055EBBC /* BottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EB12EB087F90055EBBC /* BottomSheet.swift */; }; + 94805EB92EB1E16D0055EBBC /* SessionProBottomSheet+Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EB82EB1E1650055EBBC /* SessionProBottomSheet+Models.swift */; }; + 94805EBB2EB3118F0055EBBC /* SessionProState+BottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EBA2EB311860055EBBC /* SessionProState+BottomSheet.swift */; }; + 94805EBD2EB3149F0055EBBC /* SessionProBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EBC2EB314930055EBBC /* SessionProBottomSheet.swift */; }; 9499E6032DDD9BF900091434 /* ExpandableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */; }; 9499E68B2DF92F4E00091434 /* ThreadNotificationSettingsViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */; }; 94A6B9DB2DD6BF7C00DB4B44 /* Constants+Apple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */; }; @@ -1634,6 +1637,9 @@ 947D7FE52D51837200E8E413 /* PopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverView.swift; sourceTree = ""; }; 947D7FE62D51837200E8E413 /* Text+CopyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Text+CopyButton.swift"; sourceTree = ""; }; 94805EB12EB087F90055EBBC /* BottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheet.swift; sourceTree = ""; }; + 94805EB82EB1E1650055EBBC /* SessionProBottomSheet+Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProBottomSheet+Models.swift"; sourceTree = ""; }; + 94805EBA2EB311860055EBBC /* SessionProState+BottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProState+BottomSheet.swift"; sourceTree = ""; }; + 94805EBC2EB314930055EBBC /* SessionProBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProBottomSheet.swift; sourceTree = ""; }; 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableLabel.swift; sourceTree = ""; }; 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadNotificationSettingsViewModelSpec.swift; sourceTree = ""; }; 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Constants+Apple.swift"; sourceTree = ""; }; @@ -3015,6 +3021,15 @@ path = SessionNetworkScreen; sourceTree = ""; }; + 94805EB72EB1E15B0055EBBC /* SessionProBottomSheet */ = { + isa = PBXGroup; + children = ( + 94805EB82EB1E1650055EBBC /* SessionProBottomSheet+Models.swift */, + 94805EBC2EB314930055EBBC /* SessionProBottomSheet.swift */, + ); + path = SessionProBottomSheet; + sourceTree = ""; + }; 94CD96282E1B855E0097754D /* Input View */ = { isa = PBXGroup; children = ( @@ -3784,6 +3799,7 @@ isa = PBXGroup; children = ( 94B6BAF52E30A88800E718BB /* SessionProState.swift */, + 94805EBA2EB311860055EBBC /* SessionProState+BottomSheet.swift */, FD428B1E2B4B758B006D0888 /* AppReadiness.swift */, FDE5218D2E03A06700061B8E /* AttachmentManager.swift */, FDE5219D2E0D0B9800061B8E /* AsyncAccessible.swift */, @@ -4953,6 +4969,7 @@ FD8A5AFF2DBEFB21004C689B /* Settings */ = { isa = PBXGroup; children = ( + 94805EB72EB1E15B0055EBBC /* SessionProBottomSheet */, 9438D5542E6A6843008C7FFE /* SessionProSettings */, 947D7FDC2D5180F200E8E413 /* SessionNetworkScreen */, ); @@ -6388,6 +6405,7 @@ FDE5219C2E08E76C00061B8E /* SessionAsyncImage.swift in Sources */, FD3FAB632AEB9A1500DC5421 /* ToastController.swift in Sources */, C331FFE72558FB0000070591 /* SNTextField.swift in Sources */, + 94805EB92EB1E16D0055EBBC /* SessionProBottomSheet+Models.swift in Sources */, 942256962C23F8DD00C0FDBF /* CompatibleScrollingVStack.swift in Sources */, FD71165B28E6DDBC00B47552 /* StyledNavigationController.swift in Sources */, C331FFE32558FB0000070591 /* TabBar.swift in Sources */, @@ -6433,6 +6451,7 @@ 94363E662E60186A0004EE43 /* SessionListScreen+Section.swift in Sources */, FDB11A612DD5BDCC00BEF49F /* ImageDataManager.swift in Sources */, FD6673FD2D77F54600041530 /* ScreenLockViewController.swift in Sources */, + 94805EBD2EB3149F0055EBBC /* SessionProBottomSheet.swift in Sources */, 94805EB22EB087FD0055EBBC /* BottomSheet.swift in Sources */, 94B6BB042E3B208C00E718BB /* Seperator+SwiftUI.swift in Sources */, FD8A5B222DC0489C004C689B /* AdaptiveHStack.swift in Sources */, @@ -6973,6 +6992,7 @@ B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */, FD2272762C32911C004D8A6C /* ExpirationUpdateJob.swift in Sources */, FD716E722850647600C96BF4 /* Data+Utilities.swift in Sources */, + 94805EBB2EB3118F0055EBBC /* SessionProState+BottomSheet.swift in Sources */, FD368A6829DE8F9C000DBF1E /* _026_AddFTSIfNeeded.swift in Sources */, FD5C7301284F0F7A0029977D /* MessageReceiver+UnsendRequests.swift in Sources */, C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */, diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index c44026ec23..c9c03f7928 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -537,6 +537,9 @@ extension ConversationVC: .longerMessages, beforePresented: { [weak self] in self?.hideInputAccessoryView() + }, + onConfirm: { [weak self] in + }, afterClosed: { [weak self] in self?.showInputAccessoryView() @@ -643,6 +646,9 @@ extension ConversationVC: .longerMessages, beforePresented: { [weak self] in self?.hideInputAccessoryView() + }, + onConfirm: { [weak self] in + }, afterClosed: { [weak self] in self?.showInputAccessoryView() @@ -1685,6 +1691,9 @@ extension ConversationVC: dismissType: .single, beforePresented: { [weak self] in self?.hideInputAccessoryView() + }, + onConfirm: { [weak self] in + }, afterClosed: { [weak self] in self?.showInputAccessoryView() diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 1c80b62196..220b614e30 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -345,6 +345,9 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi isSessionProActivated: (dependencies.mutate(cache: .libSession) { $0.validateSessionProState(for: threadId) }), proBadgeImage: SessionProBadge(size: .mini).toImage(using: dependencies) ), + onConfirm: { [weak self] in + + }, presenting: { modal in self?.transitionToScreen(modal, transitionType: .present) } diff --git a/Session/Media Viewing & Editing/MessageInfoScreen.swift b/Session/Media Viewing & Editing/MessageInfoScreen.swift index b2dcd11e9f..9c69acd2e7 100644 --- a/Session/Media Viewing & Editing/MessageInfoScreen.swift +++ b/Session/Media Viewing & Editing/MessageInfoScreen.swift @@ -308,6 +308,9 @@ struct MessageInfoScreen: View { .onTapGesture { dependencies[singleton: .sessionProState].showSessionProCTAIfNeeded( proCTAVariant, + onConfirm: { + + }, presenting: { modal in self.host.controller?.present(modal, animated: true) } @@ -402,6 +405,9 @@ struct MessageInfoScreen: View { .onTapGesture { dependencies[singleton: .sessionProState].showSessionProCTAIfNeeded( proCTAVariant, + onConfirm: { + + }, presenting: { modal in self.host.controller?.present(modal, animated: true) } @@ -621,6 +627,9 @@ struct MessageInfoScreen: View { onProBadgeTapped: { dependencies[singleton: .sessionProState].showSessionProCTAIfNeeded( proCTAVariant, + onConfirm: { + + }, presenting: { modal in self.host.controller?.present(modal, animated: true) } diff --git a/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift b/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift index 9b334f983a..4df8b3c17c 100644 --- a/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift +++ b/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift @@ -16,7 +16,6 @@ public class SessionProSettingsViewModel: SessionListScreenContent.ViewModelType public let navigatableState: NavigatableState = NavigatableState() public let title: String = "" public let state: SessionListScreenContent.ListItemDataState = SessionListScreenContent.ListItemDataState() - private let isBottomSheet: Bool /// This value is the current state of the view @MainActor @Published private(set) var internalState: ViewModelState @@ -24,11 +23,7 @@ public class SessionProSettingsViewModel: SessionListScreenContent.ViewModelType // MARK: - Initialization - @MainActor init( - isBottomSheet: Bool = false, - using dependencies: Dependencies - ) { - self.isBottomSheet = isBottomSheet + @MainActor init(using dependencies: Dependencies) { self.dependencies = dependencies self.internalState = ViewModelState.initialState() @@ -106,11 +101,10 @@ public class SessionProSettingsViewModel: SessionListScreenContent.ViewModelType case recoverPlan case proBadge - case largerGroups case longerMessages + case unlimitedPins case animatedDisplayPictures case badges - case unlimitedPins case plusLoadsMore case cancelPlan @@ -375,60 +369,9 @@ public class SessionProSettingsViewModel: SessionListScreenContent.ViewModelType let proFeatures: SectionModel = SectionModel( model: .proFeatures, - elements: ProFeaturesInfo.allCases(state.currentProPlanState).map { info in - SessionListScreenContent.ListItemInfo( - id: info.id, - variant: .cell( - info: .init( - leadingAccessory: .icon( - info.icon, - iconSize: .medium, - customTint: .black, - gradientBackgroundColors: info.backgroundColors, - backgroundSize: .veryLarge, - backgroundCornerRadius: 8 - ), - title: .init(info.title, font: .Headings.H9, accessory: info.accessory), - description: .init(info.description, font: .Body.smallRegular, color: .textSecondary) - ) - ) - ) - }.appending( - SessionListScreenContent.ListItemInfo( - id: .plusLoadsMore, - variant: .cell( - info: .init( - leadingAccessory: .icon( - Lucide.image(icon: .circlePlus, size: IconSize.medium.size), - iconSize: .medium, - customTint: .black, - gradientBackgroundColors: { - return switch state.currentProPlanState { - case .expired: [ThemeValue.disabled] - default: [.explicitPrimary(.orange), .explicitPrimary(.yellow)] - } - }(), - backgroundSize: .veryLarge, - backgroundCornerRadius: 8 - ), - title: .init("plusLoadsMore".localized(), font: .Headings.H9), - description: .init( - font: .Body.smallRegular, - attributedString: "plusLoadsMoreDescription" - .put(key: "pro", value: Constants.pro) - .put(key: "icon", value: Lucide.Icon.squareArrowUpRight) - .localizedFormatted(Fonts.Body.smallRegular), - color: .textSecondary - ) - ) - ), - onTap: { [weak viewModel] in viewModel?.openUrl(Constants.session_pro_roadmap) } - ) - ) + elements: getProFeaturesElements(state: state, previousState: previousState, viewModel: viewModel) ) - guard !viewModel.isBottomSheet else { return [ logo, proFeatures ] } - let proStats: SectionModel = SectionModel( model: .proStats, elements: [ @@ -577,6 +520,76 @@ public class SessionProSettingsViewModel: SessionListScreenContent.ViewModelType } } + // MARK: - Pro Features Elements + + private static func getProFeaturesElements( + state: ViewModelState, + previousState: ViewModelState, + viewModel: SessionProSettingsViewModel + ) -> [SessionListScreenContent.ListItemInfo] { + let proFeaturesIds: [ListItem] = [ .longerMessages, .unlimitedPins, .animatedDisplayPictures, .badges ] + let proFeatureInfos: [ProFeaturesInfo] = ProFeaturesInfo.allCases( + proStateExpired: { + if case .expired = state.currentProPlanState { + return true + } else { + return false + } + }() + ) + + return zip(proFeaturesIds, proFeatureInfos).map { id, info in + SessionListScreenContent.ListItemInfo( + id: id, + variant: .cell( + info: .init( + leadingAccessory: .icon( + info.icon, + iconSize: .medium, + customTint: .black, + gradientBackgroundColors: info.backgroundColors, + backgroundSize: .veryLarge, + backgroundCornerRadius: 8 + ), + title: .init(info.title, font: .Headings.H9, accessory: info.accessory), + description: .init(font: .Body.smallRegular, attributedString: info.description, color: .textSecondary) + ) + ) + ) + }.appending( + SessionListScreenContent.ListItemInfo( + id: .plusLoadsMore, + variant: .cell( + info: .init( + leadingAccessory: .icon( + Lucide.image(icon: .circlePlus, size: IconSize.medium.size), + iconSize: .medium, + customTint: .black, + gradientBackgroundColors: { + return switch state.currentProPlanState { + case .expired: [ThemeValue.disabled] + default: [.explicitPrimary(.orange), .explicitPrimary(.yellow)] + } + }(), + backgroundSize: .veryLarge, + backgroundCornerRadius: 8 + ), + title: .init("plusLoadsMore".localized(), font: .Headings.H9), + description: .init( + font: .Body.smallRegular, + attributedString: "plusLoadsMoreDescription" + .put(key: "pro", value: Constants.pro) + .put(key: "icon", value: Lucide.Icon.squareArrowUpRight) + .localizedFormatted(Fonts.Body.smallRegular), + color: .textSecondary + ) + ) + ), + onTap: { [weak viewModel] in viewModel?.openUrl(Constants.session_pro_roadmap) } + ) + ) + } + // MARK: - Pro Settings Elements private static func getProSettingsElements( @@ -951,24 +964,24 @@ extension SessionProSettingsViewModel { info: ConfirmationModal.Info( title: ( result ? - "proAccessRestored" - .put(key: "pro", value: Constants.pro) - .localized() : + "proAccessRestored" + .put(key: "pro", value: Constants.pro) + .localized() : "proAccessNotFound" - .put(key: "pro", value: Constants.pro) - .localized() + .put(key: "pro", value: Constants.pro) + .localized() ), body: .text( ( result ? - "proAccessRestoredDescription" - .put(key: "app_name", value: Constants.app_name) - .put(key: "pro", value: Constants.pro) - .localized() : + "proAccessRestoredDescription" + .put(key: "app_name", value: Constants.app_name) + .put(key: "pro", value: Constants.pro) + .localized() : "proAccessNotFoundDescription" - .put(key: "app_name", value: Constants.app_name) - .put(key: "pro", value: Constants.pro) - .localized() + .put(key: "app_name", value: Constants.app_name) + .put(key: "pro", value: Constants.pro) + .localized() ), scrollMode: .never ), @@ -985,7 +998,7 @@ extension SessionProSettingsViewModel { } ) ) - + self?.transitionToScreen(modal, transitionType: .present) } } @@ -999,8 +1012,8 @@ extension SessionProSettingsViewModel { flow: .cancel( originatingPlatform: { switch dependencies[singleton: .sessionProState].sessionProStateSubject.value.originatingPlatform { - case .iOS: return .iOS - case .Android: return .Android + case .iOS: return .iOS + case .Android: return .Android } }() ), @@ -1021,8 +1034,8 @@ extension SessionProSettingsViewModel { flow: .refund( originatingPlatform: { switch dependencies[singleton: .sessionProState].sessionProStateSubject.value.originatingPlatform { - case .iOS: return .iOS - case .Android: return .Android + case .iOS: return .iOS + case .Android: return .Android } }(), requestedAt: nil @@ -1036,96 +1049,6 @@ extension SessionProSettingsViewModel { } } -// MARK: - Pro Features Info - -extension SessionProSettingsViewModel { - struct ProFeaturesInfo { - let id: ListItem - let icon: UIImage? - let backgroundColors: [ThemeValue] - let title: String - let description: String - let accessory: SessionListScreenContent.TextInfo.Accessory - - static func allCases(_ state: SessionProPlanState) -> [ProFeaturesInfo] { - return [ - ProFeaturesInfo( - id: .largerGroups, - icon: UIImage(named: "ic_user_group_plus"), - backgroundColors: { - return switch state { - case .expired: [ThemeValue.disabled] - default: [.explicitPrimary(.green), .explicitPrimary(.blue)] - } - }(), - title: "proLargerGroups".localized(), - description: "proLargerGroupsDescription".localized(), - accessory: .none - ), - ProFeaturesInfo( - id: .longerMessages, - icon: Lucide.image(icon: .messageSquare, size: IconSize.medium.size), - backgroundColors: { - return switch state { - case .expired: [ThemeValue.disabled] - default: [.explicitPrimary(.blue), .explicitPrimary(.purple)] - } - }(), - title: "proLongerMessages".localized(), - description: "proLongerMessagesDescription".localized(), - accessory: .none - ), - ProFeaturesInfo( - id: .animatedDisplayPictures, - icon: Lucide.image(icon: .squarePlay, size: IconSize.medium.size), - backgroundColors: { - return switch state { - case .expired: [ThemeValue.disabled] - default: [.explicitPrimary(.purple), .explicitPrimary(.pink)] - } - }(), - title: "proAnimatedDisplayPictures".localized(), - description: "proAnimatedDisplayPicturesDescription".localized(), - accessory: .none - ), - ProFeaturesInfo( - id: .badges, - icon: Lucide.image(icon: .rectangleEllipsis, size: IconSize.medium.size), - backgroundColors: { - return switch state { - case .expired: [ThemeValue.disabled] - default: [.explicitPrimary(.pink), .explicitPrimary(.red)] - } - }(), - title: "proBadges".localized(), - description: "proBadgesDescription".put(key: "app_name", value: Constants.app_name).localized(), - accessory: .proBadgeLeading( - themeBackgroundColor: { - return switch state { - case .expired: .disabled - default: .primary - } - }() - ) - ), - ProFeaturesInfo( - id: .unlimitedPins, - icon: Lucide.image(icon: .pin, size: IconSize.medium.size), - backgroundColors: { - return switch state { - case .expired: [ThemeValue.disabled] - default: [.explicitPrimary(.red), .explicitPrimary(.orange)] - } - }(), - title: "proUnlimitedPins".localized(), - description: "proUnlimitedPinsDescription".localized(), - accessory: .none - ) - ] - } - } -} - // MARK: - Convenience extension SessionProPlan { diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index 5610bbbadf..f9e17d4205 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -776,12 +776,20 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl dataManager: dependencies[singleton: .imageDataManager], onProBageTapped: { [weak self, dependencies] in Task { @MainActor in - guard case .active = dependencies[singleton: .sessionProState].sessionProStateSubject.value else { return } - dependencies[singleton: .sessionProState].showSessionProCTAIfNeeded( .animatedProfileImage( isSessionProActivated: dependencies[cache: .libSession].isSessionPro ), + onConfirm: { + dependencies[singleton: .sessionProState].showSessionProBottomSheetIfNeeded( + showLoadingModal: nil, + showErrorModal: nil, + openUrl: nil, + presenting: { bottomSheet in + self?.transitionToScreen(bottomSheet, transitionType: .present) + } + ) + }, presenting: { modal in self?.transitionToScreen(modal, transitionType: .present) } @@ -834,6 +842,9 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl .animatedProfileImage( isSessionProActivated: dependencies[cache: .libSession].isSessionPro ), + onConfirm: { + + }, presenting: { modal in self?.transitionToScreen(modal, transitionType: .present) } diff --git a/SessionMessagingKit/Utilities/SessionProState+BottomSheet.swift b/SessionMessagingKit/Utilities/SessionProState+BottomSheet.swift new file mode 100644 index 0000000000..d1a48c69d9 --- /dev/null +++ b/SessionMessagingKit/Utilities/SessionProState+BottomSheet.swift @@ -0,0 +1,318 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import SessionUIKit +import SessionUtilitiesKit +import DifferenceKit + +public class SessionProBottomSheetViewModel: SessionProBottomSheetViewModelType { + public let dependencies: Dependencies + public var title: String = "" + public var state: SessionListScreenContent.ListItemDataState = SessionListScreenContent.ListItemDataState() + + /// This value is the current state of the view + @MainActor @Published private(set) var internalState: ViewModelState + private var observationTask: Task? + + // MARK: - Initialization + + @MainActor init(using dependencies: Dependencies) { + self.dependencies = dependencies + self.internalState = ViewModelState.initialState() + + self.observationTask = ObservationBuilder + .initialValue(self.internalState) + .debounce(for: .never) + .using(dependencies: dependencies) + .query(SessionProBottomSheetViewModel.queryState) + .assign { [weak self] updatedState in + guard let self = self else { return } + + self.state.updateTableData(updatedState.sections(viewModel: self)) + self.internalState = updatedState + } + } + + // MARK: - Config + + public enum Section: SessionListScreenContent.ListSection { + case logoWithPro + case proFeatures + + public var title: String? { + switch self { + case .proFeatures: return "proBetaFeatures".put(key: "pro", value: Constants.pro).localized() + default: return nil + } + } + + public var style: SessionListScreenContent.ListSectionStyle { + switch self { + case .proFeatures: return .titleNoBackgroundContent + default: return .none + } + } + + public var divider: Bool { false } + public var footer: String? { return nil } + } + + public enum ListItem: Differentiable { + case logoWithPro + case continueButton + + case longerMessages + case unlimitedPins + case animatedDisplayPictures + case badges + case plusLoadsMore + } + + // MARK: - Content + + public struct ViewModelState: ObservableKeyProvider { + let currentProPlanState: SessionProPlanState + let loadingState: SessionProLoadingState + + @MainActor public func sections(viewModel: SessionProBottomSheetViewModel) -> [SectionModel] { + SessionProBottomSheetViewModel.sections( + state: self, + viewModel: viewModel + ) + } + + public let observedKeys: Set = [ + .feature(.mockCurrentUserSessionProState), // TODO: real data from libSession + .feature(.mockCurrentUserSessionProLoadingState) // TODO: real loading status + ] + + static func initialState() -> ViewModelState { + return ViewModelState( + currentProPlanState: .none, + loadingState: .loading + ) + } + } + + @Sendable private static func queryState( + previousState: ViewModelState, + events: [ObservedEvent], + isInitialQuery: Bool, + using dependencies: Dependencies + ) async -> ViewModelState { + var currentProPlanState: SessionProPlanState = previousState.currentProPlanState + var loadingState: SessionProLoadingState = previousState.loadingState + + currentProPlanState = dependencies[singleton: .sessionProState].sessionProStateSubject.value + loadingState = dependencies[feature: .mockCurrentUserSessionProLoadingState] + + return ViewModelState( + currentProPlanState: currentProPlanState, + loadingState: loadingState + ) + } + + private static func sections( + state: ViewModelState, + viewModel: SessionProBottomSheetViewModel + ) -> [SectionModel] { + let logo: SectionModel = SectionModel( + model: .logoWithPro, + elements: [ + SessionListScreenContent.ListItemInfo( + id: .logoWithPro, + variant: .logoWithPro( + info: .init( + style:.normal, + state: { + switch state.loadingState { + case .loading: + return .loading( + message: { + if case .expired = state.currentProPlanState { + return "proStatusLoading" + .put(key: "pro", value: Constants.pro) + .localized() + } else { + return "checkingProStatus" + .put(key: "pro", value: Constants.pro) + .localized() + } + }() + ) + case .error: + return .error( + message: { + if case .expired = state.currentProPlanState { + return "proErrorRefreshingStatus" + .put(key: "pro", value: Constants.pro) + .localized() + } else { + return "errorCheckingProStatus" + .put(key: "pro", value: Constants.pro) + .localized() + } + }() + ) + case .success: + return .success + } + }(), + description: { + if case .expired = state.currentProPlanState { + return "proAccessRenewStart" + .put(key: "pro", value: Constants.pro) + .put(key: "app_pro", value: Constants.app_pro) + .localizedFormatted() + } else { + return "proFullestPotential" + .put(key: "app_name", value: Constants.app_name) + .put(key: "app_pro", value: Constants.app_pro) + .localizedFormatted() + } + }() + ) + ), + onTap: { [weak viewModel] in + switch state.loadingState { + case .loading: + viewModel?.showLoadingModal( + title: "checkingProStatus" + .put(key: "pro", value: Constants.pro) + .localized(), + description: { + if case .expired = state.currentProPlanState { + return "checkingProStatusDescription" + .put(key: "pro", value: Constants.pro) + .localized() + } else { + return "checkingProStatusContinue" + .put(key: "pro", value: Constants.pro) + .localized() + } + }() + ) + case .error: + viewModel?.showErrorModal( + title: "proStatusError" + .put(key: "pro", value: Constants.pro) + .localized(), + description: "proStatusRefreshNetworkError" + .put(key: "pro", value: Constants.pro) + .localizedFormatted() + ) + case .success: + break + } + } + ), + SessionListScreenContent.ListItemInfo( + id: .continueButton, + variant: .button(title: "theContinue".localized(), enabled: (state.loadingState == .success)), + onTap: { [weak viewModel] in + switch state.loadingState { + case .loading: + viewModel?.showLoadingModal( + title: "checkingProStatus" + .put(key: "pro", value: Constants.pro) + .localized(), + description: "checkingProStatusContinue" + .put(key: "pro", value: Constants.pro) + .localized() + ) + case .error: + viewModel?.showErrorModal( + title: "proStatusError" + .put(key: "pro", value: Constants.pro) + .localized(), + description: "proStatusRefreshNetworkError" + .put(key: "pro", value: Constants.pro) + .localizedFormatted() + ) + case .success: + break +// viewModel?.updateProPlan() + } + } + ) + ] + ) + + let proFeatures: SectionModel = SectionModel( + model: .proFeatures, + elements: getProFeaturesElements(state: state, viewModel: viewModel) + ) + + return [ logo, proFeatures ] + } + + // MARK: - Pro Features Elements + + private static func getProFeaturesElements( + state: ViewModelState, + viewModel: SessionProBottomSheetViewModel + ) -> [SessionListScreenContent.ListItemInfo] { + let proFeaturesIds: [ListItem] = [ .longerMessages, .unlimitedPins, .animatedDisplayPictures, .badges ] + let proFeatureInfos: [ProFeaturesInfo] = ProFeaturesInfo.allCases(proStateExpired: false) + let plusMoreFeatureInfo: ProFeaturesInfo = ProFeaturesInfo.plusMoreFeatureInfo(proStateExpired: false) + + var result = zip(proFeaturesIds, proFeatureInfos).map { id, info in + SessionListScreenContent.ListItemInfo( + id: id, + variant: .cell( + info: .init( + leadingAccessory: .icon( + info.icon, + iconSize: .medium, + customTint: .black, + gradientBackgroundColors: info.backgroundColors, + backgroundSize: .veryLarge, + backgroundCornerRadius: 8 + ), + title: .init(info.title, font: .Headings.H9, accessory: info.accessory), + description: .init(font: .Body.smallRegular, attributedString: info.description, color: .textSecondary) + ) + ) + ) + } + result.append( + SessionListScreenContent.ListItemInfo( + id: .plusLoadsMore, + variant: .cell( + info: .init( + leadingAccessory: .icon( + plusMoreFeatureInfo.icon, + iconSize: .medium, + customTint: .black, + gradientBackgroundColors: plusMoreFeatureInfo.backgroundColors, + backgroundSize: .veryLarge, + backgroundCornerRadius: 8 + ), + title: .init(plusMoreFeatureInfo.title, font: .Headings.H9), + description: .init( + font: .Body.smallRegular, + attributedString: plusMoreFeatureInfo.description, + color: .textSecondary + ) + ) + ), + onTap: { [weak viewModel] in + viewModel?.openUrl(Constants.session_pro_roadmap) + } + ) + ) + + return result + } + + public func showLoadingModal(title: String, description: String) { + + } + + public func showErrorModal(title: String, description: ThemedAttributedString) { + + } + + public func openUrl(_ urlString: String) { + + } +} diff --git a/SessionMessagingKit/Utilities/SessionProState.swift b/SessionMessagingKit/Utilities/SessionProState.swift index c09e751cb1..a61ecd8d7b 100644 --- a/SessionMessagingKit/Utilities/SessionProState.swift +++ b/SessionMessagingKit/Utilities/SessionProState.swift @@ -15,7 +15,7 @@ public extension Singleton { // MARK: - SessionProState -public class SessionProState: SessionProManagerType, ProfilePictureAnimationManagerType, SessionProCTAManagerType { +public class SessionProState: SessionProManagerType, ProfilePictureAnimationManagerType { public let dependencies: Dependencies public var sessionProStateSubject: CurrentValueSubject public var sessionProStatePublisher: AnyPublisher { @@ -142,17 +142,22 @@ public class SessionProState: SessionProManagerType, ProfilePictureAnimationMana .with(originatingPlatform: newValue) ) } - +} + +// MARK: - SessionProCTAManagerType + +extension SessionProState: SessionProCTAManagerType { @discardableResult @MainActor public func showSessionProCTAIfNeeded( _ variant: ProCTAModal.Variant, dismissType: Modal.DismissType, beforePresented: (() -> Void)?, + onConfirm: (() -> Void)?, afterClosed: (() -> Void)?, presenting: ((UIViewController) -> Void)? ) -> Bool { - guard dependencies[feature: .sessionProEnabled] else { - return false - } + guard dependencies[feature: .sessionProEnabled] else { return false } + if case .active = sessionProStateSubject.value { return false } + beforePresented?() let sessionProModal: ModalHostingViewController = ModalHostingViewController( modal: ProCTAModal( @@ -160,13 +165,25 @@ public class SessionProState: SessionProManagerType, ProfilePictureAnimationMana dataManager: dependencies[singleton: .imageDataManager], dismissType: dismissType, afterClosed: afterClosed, - onConfirm: { - - } + onConfirm: onConfirm ) ) presenting?(sessionProModal) return true } + + @MainActor public func showSessionProBottomSheetIfNeeded( + showLoadingModal: ((String, String) -> Void)?, + showErrorModal: ((String, ThemedAttributedString) -> Void)?, + openUrl: ((URL) -> Void)?, + presenting: ((UIViewController) -> Void)? + ) { + let sessionProBottomSheet: BottomSheetHostingViewController = BottomSheetHostingViewController( + bottomSheet: BottomSheet(hasCloseButton: true) { [dependencies] in + SessionListScreen(viewModel: SessionProBottomSheetViewModel(using: dependencies)) + } + ) + presenting?(sessionProBottomSheet) + } } diff --git a/SessionUIKit/Components/SwiftUI/BottomSheet.swift b/SessionUIKit/Components/SwiftUI/BottomSheet.swift index 65a4aeab70..a7afd5b3b8 100644 --- a/SessionUIKit/Components/SwiftUI/BottomSheet.swift +++ b/SessionUIKit/Components/SwiftUI/BottomSheet.swift @@ -4,17 +4,20 @@ import SwiftUI import Lucide public struct BottomSheet: View where Content: View { - let host: HostWrapper - let dismissType: Modal.DismissType + @EnvironmentObject var host: HostWrapper let hasCloseButton: Bool - let afterClosed: (() -> Void)? - let content: (@escaping ((() -> Void)?) -> Void) -> Content + let content: () -> Content let cornerRadius: CGFloat = 11 let shadowRadius: CGFloat = 10 let shadowOpacity: Double = 0.4 @State private var show: Bool = true + + public init(hasCloseButton: Bool, content: @escaping () -> Content) { + self.hasCloseButton = hasCloseButton + self.content = content + } public var body: some View { ZStack(alignment: .bottom) { @@ -23,20 +26,17 @@ public struct BottomSheet: View where Content: View { .fill(.ultraThinMaterial) .frame(maxWidth: .infinity, maxHeight: .infinity) .ignoresSafeArea() - .onTapGesture { close() } // Bottom Sheet - VStack { + VStack() { Spacer() ZStack(alignment: .topTrailing) { - content { internalAfterClosed in - close(internalAfterClosed) - } + content() if hasCloseButton { Button { - close(nil) + close() } label: { AttributedText(Lucide.Icon.x.attributedString(size: 20)) .font(.system(size: 20)) @@ -57,6 +57,7 @@ public struct BottomSheet: View where Content: View { .transition(.move(edge: .bottom).combined(with: .opacity)) .animation(.spring(), value: show) } + .ignoresSafeArea(edges: .bottom) .frame( maxWidth: .infinity, maxHeight: .infinity, @@ -74,14 +75,8 @@ public struct BottomSheet: View where Content: View { // MARK: - Dismiss Logic - private func close(_ internalAfterClosed: (() -> Void)? = nil) { - host.controller?.presentingViewController?.dismiss( - animated: true, - completion: { - afterClosed?() - internalAfterClosed?() - } - ) + private func close() { + host.controller?.presentingViewController?.dismiss(animated: true) } } diff --git a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift index c7ad2e99ce..ad7a0139e3 100644 --- a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift +++ b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift @@ -41,7 +41,7 @@ public struct ProCTAModal: View { ZStack { if let animatedAvatarImageURL = variant.animatedAvatarImageURL { GeometryReader { geometry in - let size: CGFloat = geometry.size.width / 1522.0 * 135 + let size: CGFloat = geometry.size.width / 1522.0 * variant.animatedAvatarImageSize let scale: CGFloat = geometry.size.width / 1522.0 SessionAsyncImage( source: .url(animatedAvatarImageURL), @@ -225,8 +225,8 @@ public struct ProCTAModal: View { HStack(spacing: Values.smallSpacing) { // Upgrade Button ShineButton { - onConfirm?() close(nil) + onConfirm?() } label: { Text(variant.confirmButtonTitle) .font(.Body.baseRegular) @@ -320,10 +320,18 @@ public extension ProCTAModal { public var animatedAvatarImagePadding: (leading: CGFloat, top: CGFloat) { switch self { case .generic: return (1293, 743) - case .animatedProfileImage: return (690, 363) + case .animatedProfileImage: return (680, 363) default: return (0, 0) } } + + public var animatedAvatarImageSize: CGFloat { + switch self { + case .generic: return 135 + case .animatedProfileImage: return 200 + default: return 0 + } + } public var subtitle: String { switch self { @@ -463,9 +471,17 @@ public protocol SessionProCTAManagerType: AnyObject { _ variant: ProCTAModal.Variant, dismissType: Modal.DismissType, beforePresented: (() -> Void)?, + onConfirm: (() -> Void)?, afterClosed: (() -> Void)?, presenting: ((UIViewController) -> Void)? ) -> Bool + + @MainActor func showSessionProBottomSheetIfNeeded( + showLoadingModal: ((String, String) -> Void)?, + showErrorModal: ((String, ThemedAttributedString) -> Void)?, + openUrl: ((URL) -> Void)?, + presenting: ((UIViewController) -> Void)? + ) } // MARK: - Convenience @@ -474,6 +490,7 @@ public extension SessionProCTAManagerType { @discardableResult @MainActor func showSessionProCTAIfNeeded( _ variant: ProCTAModal.Variant, beforePresented: (() -> Void)?, + onConfirm: (() -> Void)?, afterClosed: (() -> Void)?, presenting: ((UIViewController) -> Void)? ) -> Bool { @@ -481,6 +498,7 @@ public extension SessionProCTAManagerType { variant, dismissType: .recursive, beforePresented: beforePresented, + onConfirm: onConfirm, afterClosed: afterClosed, presenting: presenting ) @@ -488,12 +506,14 @@ public extension SessionProCTAManagerType { @discardableResult @MainActor func showSessionProCTAIfNeeded( _ variant: ProCTAModal.Variant, + onConfirm: (() -> Void)?, presenting: ((UIViewController) -> Void)? ) -> Bool { showSessionProCTAIfNeeded( variant, dismissType: .recursive, beforePresented: nil, + onConfirm: onConfirm, afterClosed: nil, presenting: presenting ) diff --git a/SessionUIKit/Screens/Settings/SessionProBottomSheet/SessionProBottomSheet+Models.swift b/SessionUIKit/Screens/Settings/SessionProBottomSheet/SessionProBottomSheet+Models.swift new file mode 100644 index 0000000000..f5afb22587 --- /dev/null +++ b/SessionUIKit/Screens/Settings/SessionProBottomSheet/SessionProBottomSheet+Models.swift @@ -0,0 +1,67 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import DifferenceKit +import Lucide + +public protocol SessionProBottomSheetViewModelType: SessionListScreenContent.ViewModelType { + func showLoadingModal(title: String, description: String) + func showErrorModal(title: String, description: ThemedAttributedString) + func openUrl(_ urlString: String) +} + +// MARK: - Pro Features Info + +public struct ProFeaturesInfo { + public let icon: UIImage? + public let backgroundColors: [ThemeValue] + public let title: String + public let description: ThemedAttributedString + public let accessory: SessionListScreenContent.TextInfo.Accessory + + public static func allCases(proStateExpired: Bool) -> [ProFeaturesInfo] { + return [ + ProFeaturesInfo( + icon: Lucide.image(icon: .messageSquare, size: IconSize.medium.size), + backgroundColors: proStateExpired ? [ThemeValue.disabled] : [.explicitPrimary(.blue), .explicitPrimary(.purple)], + title: "proLongerMessages".localized(), + description: "proLongerMessagesDescription".localizedFormatted(baseFont: Fonts.Body.smallRegular), + accessory: .none + ), + ProFeaturesInfo( + icon: Lucide.image(icon: .pin, size: IconSize.medium.size), + backgroundColors: proStateExpired ? [ThemeValue.disabled] : [.explicitPrimary(.purple), .explicitPrimary(.pink)], + title: "proUnlimitedPins".localized(), + description: "proUnlimitedPinsDescription".localizedFormatted(baseFont: Fonts.Body.smallRegular), + accessory: .none + ), + ProFeaturesInfo( + icon: Lucide.image(icon: .squarePlay, size: IconSize.medium.size), + backgroundColors: proStateExpired ? [ThemeValue.disabled] : [.explicitPrimary(.pink), .explicitPrimary(.red)], + title: "proAnimatedDisplayPictures".localized(), + description: "proAnimatedDisplayPicturesDescription".localizedFormatted(baseFont: Fonts.Body.smallRegular), + accessory: .none + ), + ProFeaturesInfo( + icon: Lucide.image(icon: .rectangleEllipsis, size: IconSize.medium.size), + backgroundColors: proStateExpired ? [ThemeValue.disabled] : [.explicitPrimary(.red), .explicitPrimary(.orange)], + title: "proBadges".localized(), + description: "proBadgesDescription".put(key: "app_name", value: Constants.app_name).localizedFormatted(Fonts.Body.smallRegular), + accessory: .proBadgeLeading(themeBackgroundColor: proStateExpired ? .disabled : .primary) + ) + ] + } + + public static func plusMoreFeatureInfo(proStateExpired: Bool) -> ProFeaturesInfo { + ProFeaturesInfo( + icon: Lucide.image(icon: .circlePlus, size: IconSize.medium.size), + backgroundColors: proStateExpired ? [ThemeValue.disabled] : [.explicitPrimary(.orange), .explicitPrimary(.yellow)], + title: "plusLoadsMore".localized(), + description: "plusLoadsMoreDescription" + .put(key: "pro", value: Constants.pro) + .put(key: "icon", value: Lucide.Icon.squareArrowUpRight) + .localizedFormatted(Fonts.Body.smallRegular), + accessory: .proBadgeLeading(themeBackgroundColor: proStateExpired ? .disabled : .primary) + ) + } +} diff --git a/SessionUIKit/Screens/Settings/SessionProBottomSheet/SessionProBottomSheet.swift b/SessionUIKit/Screens/Settings/SessionProBottomSheet/SessionProBottomSheet.swift new file mode 100644 index 0000000000..39fe552bda --- /dev/null +++ b/SessionUIKit/Screens/Settings/SessionProBottomSheet/SessionProBottomSheet.swift @@ -0,0 +1,26 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import SwiftUI +import Lucide + +public struct SessionProBottomSheet: View { + @EnvironmentObject var host: HostWrapper + + let viewModel: any SessionProBottomSheetViewModelType + let hasCloseButton: Bool + let afterClosed: (() -> Void)? + + public init( + viewModel: any SessionProBottomSheetViewModelType, + hasCloseButton: Bool, + afterClosed: (() -> Void)? + ) { + self.viewModel = viewModel + self.hasCloseButton = hasCloseButton + self.afterClosed = afterClosed + } + + public var body: some View { + + } +} diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift index e3d4039eb8..8f4be37982 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift @@ -643,6 +643,9 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC .longerMessages, beforePresented: { [weak self] in self?.hideInputAccessoryView() + }, + onConfirm: { [weak self] in + }, afterClosed: { [weak self] in self?.showInputAccessoryView() @@ -684,6 +687,9 @@ extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate { .longerMessages, beforePresented: { [weak self] in self?.hideInputAccessoryView() + }, + onConfirm: { [weak self] in + }, afterClosed: { [weak self] in self?.showInputAccessoryView() From c17ab47f6b3e315419a9af6668785bc088efedba Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Fri, 31 Oct 2025 12:22:45 +1100 Subject: [PATCH 06/60] fix: bottom sheet height --- Session.xcodeproj/project.pbxproj | 4 -- .../Components/SwiftUI/BottomSheet.swift | 42 +++++++++---------- .../Components/SwiftUI/ShineButton.swift | 2 +- .../SessionProBottomSheet.swift | 26 ------------ 4 files changed, 21 insertions(+), 53 deletions(-) delete mode 100644 SessionUIKit/Screens/Settings/SessionProBottomSheet/SessionProBottomSheet.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index a7ae256c1f..f5ff371788 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -216,7 +216,6 @@ 94805EB22EB087FD0055EBBC /* BottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EB12EB087F90055EBBC /* BottomSheet.swift */; }; 94805EB92EB1E16D0055EBBC /* SessionProBottomSheet+Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EB82EB1E1650055EBBC /* SessionProBottomSheet+Models.swift */; }; 94805EBB2EB3118F0055EBBC /* SessionProState+BottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EBA2EB311860055EBBC /* SessionProState+BottomSheet.swift */; }; - 94805EBD2EB3149F0055EBBC /* SessionProBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EBC2EB314930055EBBC /* SessionProBottomSheet.swift */; }; 9499E6032DDD9BF900091434 /* ExpandableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */; }; 9499E68B2DF92F4E00091434 /* ThreadNotificationSettingsViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */; }; 94A6B9DB2DD6BF7C00DB4B44 /* Constants+Apple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */; }; @@ -1639,7 +1638,6 @@ 94805EB12EB087F90055EBBC /* BottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheet.swift; sourceTree = ""; }; 94805EB82EB1E1650055EBBC /* SessionProBottomSheet+Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProBottomSheet+Models.swift"; sourceTree = ""; }; 94805EBA2EB311860055EBBC /* SessionProState+BottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProState+BottomSheet.swift"; sourceTree = ""; }; - 94805EBC2EB314930055EBBC /* SessionProBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProBottomSheet.swift; sourceTree = ""; }; 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableLabel.swift; sourceTree = ""; }; 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadNotificationSettingsViewModelSpec.swift; sourceTree = ""; }; 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Constants+Apple.swift"; sourceTree = ""; }; @@ -3025,7 +3023,6 @@ isa = PBXGroup; children = ( 94805EB82EB1E1650055EBBC /* SessionProBottomSheet+Models.swift */, - 94805EBC2EB314930055EBBC /* SessionProBottomSheet.swift */, ); path = SessionProBottomSheet; sourceTree = ""; @@ -6451,7 +6448,6 @@ 94363E662E60186A0004EE43 /* SessionListScreen+Section.swift in Sources */, FDB11A612DD5BDCC00BEF49F /* ImageDataManager.swift in Sources */, FD6673FD2D77F54600041530 /* ScreenLockViewController.swift in Sources */, - 94805EBD2EB3149F0055EBBC /* SessionProBottomSheet.swift in Sources */, 94805EB22EB087FD0055EBBC /* BottomSheet.swift in Sources */, 94B6BB042E3B208C00E718BB /* Seperator+SwiftUI.swift in Sources */, FD8A5B222DC0489C004C689B /* AdaptiveHStack.swift in Sources */, diff --git a/SessionUIKit/Components/SwiftUI/BottomSheet.swift b/SessionUIKit/Components/SwiftUI/BottomSheet.swift index a7afd5b3b8..536d198d19 100644 --- a/SessionUIKit/Components/SwiftUI/BottomSheet.swift +++ b/SessionUIKit/Components/SwiftUI/BottomSheet.swift @@ -13,6 +13,7 @@ public struct BottomSheet: View where Content: View { let shadowOpacity: Double = 0.4 @State private var show: Bool = true + @State private var contentHeight: CGFloat = 80 public init(hasCloseButton: Bool, content: @escaping () -> Content) { self.hasCloseButton = hasCloseButton @@ -28,32 +29,29 @@ public struct BottomSheet: View where Content: View { .ignoresSafeArea() // Bottom Sheet - VStack() { - Spacer() + ZStack(alignment: .topTrailing) { + content() - ZStack(alignment: .topTrailing) { - content() - - if hasCloseButton { - Button { - close() - } label: { - AttributedText(Lucide.Icon.x.attributedString(size: 20)) - .font(.system(size: 20)) - .foregroundColor(themeColor: .textPrimary) - } - .frame(width: 24, height: 24) - .padding(Values.mediumSmallSpacing) + if hasCloseButton { + Button { + close() + } label: { + AttributedText(Lucide.Icon.x.attributedString(size: 20)) + .font(.system(size: 20)) + .foregroundColor(themeColor: .textPrimary) } + .frame(width: 24, height: 24) + .padding(Values.mediumSmallSpacing) } - .backgroundColor(themeColor: .alert_background) - .cornerRadius(cornerRadius, corners: [.topLeft, .topRight]) - .shadow(color: Color.black.opacity(shadowOpacity), radius: shadowRadius) - .frame( - maxWidth: .infinity, - alignment: .topTrailing - ) } + .backgroundColor(themeColor: .backgroundPrimary) + .cornerRadius(cornerRadius, corners: [.topLeft, .topRight]) + .shadow(color: Color.black.opacity(shadowOpacity), radius: shadowRadius) + .frame( + maxWidth: .infinity, + alignment: .topTrailing + ) + .padding(.top, 80) .transition(.move(edge: .bottom).combined(with: .opacity)) .animation(.spring(), value: show) } diff --git a/SessionUIKit/Components/SwiftUI/ShineButton.swift b/SessionUIKit/Components/SwiftUI/ShineButton.swift index 479cfeae69..3ded9162e5 100644 --- a/SessionUIKit/Components/SwiftUI/ShineButton.swift +++ b/SessionUIKit/Components/SwiftUI/ShineButton.swift @@ -64,7 +64,7 @@ struct ShineOverlay: View { endPoint: .trailing ) ) - .frame(width: width * 0.4, height: height) // 0.4 让光带不是整块 + .frame(width: width * 0.4, height: height) .offset( x: shineX * width, y: 0 diff --git a/SessionUIKit/Screens/Settings/SessionProBottomSheet/SessionProBottomSheet.swift b/SessionUIKit/Screens/Settings/SessionProBottomSheet/SessionProBottomSheet.swift deleted file mode 100644 index 39fe552bda..0000000000 --- a/SessionUIKit/Screens/Settings/SessionProBottomSheet/SessionProBottomSheet.swift +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. - -import SwiftUI -import Lucide - -public struct SessionProBottomSheet: View { - @EnvironmentObject var host: HostWrapper - - let viewModel: any SessionProBottomSheetViewModelType - let hasCloseButton: Bool - let afterClosed: (() -> Void)? - - public init( - viewModel: any SessionProBottomSheetViewModelType, - hasCloseButton: Bool, - afterClosed: (() -> Void)? - ) { - self.viewModel = viewModel - self.hasCloseButton = hasCloseButton - self.afterClosed = afterClosed - } - - public var body: some View { - - } -} From 1a1af596fb268bcbbe75e2b0ea8d1f5824a24308 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Fri, 31 Oct 2025 17:06:02 +1100 Subject: [PATCH 07/60] wip: bottom sheet navigation --- Session.xcodeproj/project.pbxproj | 8 +-- .../Components/SwiftUI/BottomSheet.swift | 69 ++++++++++++++++++- .../Types/TransitionType.swift | 2 +- 3 files changed, 73 insertions(+), 6 deletions(-) rename {Session/Shared => SessionUIKit}/Types/TransitionType.swift (56%) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index f5ff371788..458204a43d 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -216,6 +216,7 @@ 94805EB22EB087FD0055EBBC /* BottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EB12EB087F90055EBBC /* BottomSheet.swift */; }; 94805EB92EB1E16D0055EBBC /* SessionProBottomSheet+Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EB82EB1E1650055EBBC /* SessionProBottomSheet+Models.swift */; }; 94805EBB2EB3118F0055EBBC /* SessionProState+BottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EBA2EB311860055EBBC /* SessionProState+BottomSheet.swift */; }; + 94805EBF2EB462C40055EBBC /* TransitionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EBE2EB462C10055EBBC /* TransitionType.swift */; }; 9499E6032DDD9BF900091434 /* ExpandableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */; }; 9499E68B2DF92F4E00091434 /* ThreadNotificationSettingsViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */; }; 94A6B9DB2DD6BF7C00DB4B44 /* Constants+Apple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */; }; @@ -862,7 +863,6 @@ FD71163828E2C50700B47552 /* SessionTableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0828AA2D27003AE748 /* SessionTableViewModel.swift */; }; FD71163E28E2C82900B47552 /* SessionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0A28AB12E2003AE748 /* SessionCell.swift */; }; FD71163F28E2C82C00B47552 /* SessionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115EF28C5D7DE00B47552 /* SessionHeaderView.swift */; }; - FD71164228E2C85A00B47552 /* TransitionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71163328E2C48400B47552 /* TransitionType.swift */; }; FD71164428E2CB8A00B47552 /* SessionCell+Accessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164328E2CB8A00B47552 /* SessionCell+Accessory.swift */; }; FD71164628E2CC1300B47552 /* SessionHighlightingBackgroundLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164528E2CC1300B47552 /* SessionHighlightingBackgroundLabel.swift */; }; FD71164828E2CE8700B47552 /* SessionCell+AccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164728E2CE8700B47552 /* SessionCell+AccessoryView.swift */; }; @@ -1638,6 +1638,7 @@ 94805EB12EB087F90055EBBC /* BottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheet.swift; sourceTree = ""; }; 94805EB82EB1E1650055EBBC /* SessionProBottomSheet+Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProBottomSheet+Models.swift"; sourceTree = ""; }; 94805EBA2EB311860055EBBC /* SessionProState+BottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProState+BottomSheet.swift"; sourceTree = ""; }; + 94805EBE2EB462C10055EBBC /* TransitionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionType.swift; sourceTree = ""; }; 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableLabel.swift; sourceTree = ""; }; 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadNotificationSettingsViewModelSpec.swift; sourceTree = ""; }; 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Constants+Apple.swift"; sourceTree = ""; }; @@ -2175,7 +2176,6 @@ FD71162B28E1451400B47552 /* Position.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Position.swift; sourceTree = ""; }; FD71162D28E168C700B47552 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; FD71163128E2C42A00B47552 /* IconSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSize.swift; sourceTree = ""; }; - FD71163328E2C48400B47552 /* TransitionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionType.swift; sourceTree = ""; }; FD71164328E2CB8A00B47552 /* SessionCell+Accessory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCell+Accessory.swift"; sourceTree = ""; }; FD71164528E2CC1300B47552 /* SessionHighlightingBackgroundLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionHighlightingBackgroundLabel.swift; sourceTree = ""; }; FD71164728E2CE8700B47552 /* SessionCell+AccessoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCell+AccessoryView.swift"; sourceTree = ""; }; @@ -4760,6 +4760,7 @@ FD71163028E2C41900B47552 /* Types */ = { isa = PBXGroup; children = ( + 94805EBE2EB462C10055EBBC /* TransitionType.swift */, FDE6E99729F8E63A00F93C5D /* Accessibility.swift */, FD71163128E2C42A00B47552 /* IconSize.swift */, FDB11A602DD5BDC900BEF49F /* ImageDataManager.swift */, @@ -4792,7 +4793,6 @@ FD12A8422AD63BF600EEBA0D /* ObservableTableSource.swift */, FD12A8442AD63C2200EEBA0D /* TableDataState.swift */, FD12A8462AD63C3400EEBA0D /* PagedObservationSource.swift */, - FD71163328E2C48400B47552 /* TransitionType.swift */, FD71164D28E3F8CC00B47552 /* SessionCell+Info.swift */, FD71164328E2CB8A00B47552 /* SessionCell+Accessory.swift */, FDF848F429413EEC007DCAE5 /* SessionCell+Styling.swift */, @@ -6439,6 +6439,7 @@ FD8A5B0D2DBF2CA1004C689B /* Localization.swift in Sources */, 945E89D62E9602AB00D8D907 /* SessionProPaymentScreen+Purchase.swift in Sources */, FD37E9F628A5F106003AE748 /* Configuration.swift in Sources */, + 94805EBF2EB462C40055EBBC /* TransitionType.swift in Sources */, 94AAB1512E1F753500A6FA18 /* CyclicGradientView.swift in Sources */, FD8A5B1E2DBF4BBC004C689B /* ScreenLock+Errors.swift in Sources */, 942BA9412E4487F7007C4595 /* LightBox.swift in Sources */, @@ -7180,7 +7181,6 @@ 7BAADFCC27B0EF23007BCF92 /* CallVideoView.swift in Sources */, 942256892C23F8C800C0FDBF /* LandingScreen.swift in Sources */, B8CCF63F23975CFB0091D419 /* JoinOpenGroupVC.swift in Sources */, - FD71164228E2C85A00B47552 /* TransitionType.swift in Sources */, 942256882C23F8C800C0FDBF /* PNModeScreen.swift in Sources */, FD37E9DB28A244E9003AE748 /* ThemeMessagePreviewView.swift in Sources */, FD7443422D07A27E00862443 /* SyncPushTokensJob.swift in Sources */, diff --git a/SessionUIKit/Components/SwiftUI/BottomSheet.swift b/SessionUIKit/Components/SwiftUI/BottomSheet.swift index 536d198d19..85ddff14d7 100644 --- a/SessionUIKit/Components/SwiftUI/BottomSheet.swift +++ b/SessionUIKit/Components/SwiftUI/BottomSheet.swift @@ -2,11 +2,14 @@ import SwiftUI import Lucide +import Combine public struct BottomSheet: View where Content: View { @EnvironmentObject var host: HostWrapper + let navigatableState: BottomSheetNavigatableState let hasCloseButton: Bool let content: () -> Content + private var disposables: Set = Set() let cornerRadius: CGFloat = 11 let shadowRadius: CGFloat = 10 @@ -15,9 +18,15 @@ public struct BottomSheet: View where Content: View { @State private var show: Bool = true @State private var contentHeight: CGFloat = 80 - public init(hasCloseButton: Bool, content: @escaping () -> Content) { + public init( + navigatableState: BottomSheetNavigatableState, + hasCloseButton: Bool, + content: @escaping () -> Content) + { + self.navigatableState = navigatableState self.hasCloseButton = hasCloseButton self.content = content + navigatableState.setupBindings(viewController: host.controller, disposables: &disposables) } public var body: some View { @@ -104,3 +113,61 @@ open class BottomSheetHostingViewController: UIHostingController + + // MARK: - Internal Variables + + fileprivate let _transitionToScreen: PassthroughSubject<(UIViewController, TransitionType), Never> = PassthroughSubject() + + // MARK: - Initialization + + init() { + self.transitionToScreen = _transitionToScreen + .subscribe(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + // MARK: - Functions + + public func setupBindings( + viewController: UIViewController?, + disposables: inout Set + ) { + self.transitionToScreen + .receive(on: DispatchQueue.main) + .sink { [weak viewController] targetViewController, transitionType in + switch transitionType { + case .push: + viewController?.navigationController?.pushViewController(targetViewController, animated: true) + + case .present: + let presenter: UIViewController? = (viewController?.presentedViewController ?? viewController) + + if UIDevice.current.isIPad { + targetViewController.popoverPresentationController?.permittedArrowDirections = [] + targetViewController.popoverPresentationController?.sourceView = presenter?.view + targetViewController.popoverPresentationController?.sourceRect = (presenter?.view.bounds ?? UIScreen.main.bounds) + } + + presenter?.present(targetViewController, animated: true) + } + } + .store(in: &disposables) + } +} diff --git a/Session/Shared/Types/TransitionType.swift b/SessionUIKit/Types/TransitionType.swift similarity index 56% rename from Session/Shared/Types/TransitionType.swift rename to SessionUIKit/Types/TransitionType.swift index c4915aaacc..d5523e0cee 100644 --- a/Session/Shared/Types/TransitionType.swift +++ b/SessionUIKit/Types/TransitionType.swift @@ -1,4 +1,4 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. import Foundation From 69125c703fa491b1010f6f2437747866ca7f4ce3 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Mon, 3 Nov 2025 14:58:48 +1100 Subject: [PATCH 08/60] wip: swiftui navigatable state --- Session.xcodeproj/project.pbxproj | 40 ++++--- Session/Settings/ImagePickerHandler.swift | 1 + .../SessionProSettingsViewModel.swift | 101 ----------------- .../SessionProPaymentScreenContent.swift | 6 +- .../SessionProState+BottomSheet.swift | 21 +++- .../SessionPro/SessionProState+Models.swift | 103 ++++++++++++++++++ .../{ => SessionPro}/SessionProState.swift | 14 +-- .../Components/SwiftUI/BottomSheet.swift | 73 ++----------- .../SessionProPaymentScreen+Models.swift | 1 - .../SessionProPaymentScreen.swift | 10 +- .../SessionProPlanUpdatedScreen.swift | 9 +- .../Shared/SessionListScreen+Models.swift | 100 +++++++++++++++++ .../Screens/Shared/SessionListScreen.swift | 53 ++++++++- .../Types/DismissType.swift | 0 .../UINavigationController+Utilities.swift | 0 15 files changed, 328 insertions(+), 204 deletions(-) rename Session/Settings/SessionProSettings/SessionProPaymentScreen+ViewModel.swift => SessionMessagingKit/Utilities/SessionPro/SessionProPaymentScreenContent.swift (86%) rename SessionMessagingKit/Utilities/{ => SessionPro}/SessionProState+BottomSheet.swift (94%) create mode 100644 SessionMessagingKit/Utilities/SessionPro/SessionProState+Models.swift rename SessionMessagingKit/Utilities/{ => SessionPro}/SessionProState.swift (93%) rename {Session/Shared => SessionUIKit}/Types/DismissType.swift (100%) rename {SessionUtilitiesKit => SessionUIKit}/Utilities/UINavigationController+Utilities.swift (100%) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 458204a43d..4bc910e083 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -217,6 +217,10 @@ 94805EB92EB1E16D0055EBBC /* SessionProBottomSheet+Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EB82EB1E1650055EBBC /* SessionProBottomSheet+Models.swift */; }; 94805EBB2EB3118F0055EBBC /* SessionProState+BottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EBA2EB311860055EBBC /* SessionProState+BottomSheet.swift */; }; 94805EBF2EB462C40055EBBC /* TransitionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EBE2EB462C10055EBBC /* TransitionType.swift */; }; + 94805EC12EB48D910055EBBC /* SessionProState+Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EC02EB48D860055EBBC /* SessionProState+Models.swift */; }; + 94805EC32EB48ED50055EBBC /* SessionProPaymentScreenContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EC22EB48EC40055EBBC /* SessionProPaymentScreenContent.swift */; }; + 94805EC62EB823B80055EBBC /* DismissType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EC52EB823B00055EBBC /* DismissType.swift */; }; + 94805EC82EB834D40055EBBC /* UINavigationController+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EC72EB834CD0055EBBC /* UINavigationController+Utilities.swift */; }; 9499E6032DDD9BF900091434 /* ExpandableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */; }; 9499E68B2DF92F4E00091434 /* ThreadNotificationSettingsViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */; }; 94A6B9DB2DD6BF7C00DB4B44 /* Constants+Apple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */; }; @@ -253,7 +257,6 @@ 94D716822E8FA1A0008294EE /* AttributedLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D716812E8FA19D008294EE /* AttributedLabel.swift */; }; 94D716862E933958008294EE /* SessionProBadge+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D716852E93394B008294EE /* SessionProBadge+Utilities.swift */; }; 94D716912E9379BA008294EE /* MentionUtilities+Attributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D716902E9379A4008294EE /* MentionUtilities+Attributes.swift */; }; - 94D955EB2E9CA5E000DEE66E /* SessionProPaymentScreen+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D955EA2E9CA5DA00DEE66E /* SessionProPaymentScreen+ViewModel.swift */; }; 94E9BC0D2C7BFBDA006984EA /* Localization+Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */; }; A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; }; A163E8AB16F3F6AA0094D68B /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A163E8AA16F3F6A90094D68B /* Security.framework */; }; @@ -866,7 +869,6 @@ FD71164428E2CB8A00B47552 /* SessionCell+Accessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164328E2CB8A00B47552 /* SessionCell+Accessory.swift */; }; FD71164628E2CC1300B47552 /* SessionHighlightingBackgroundLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164528E2CC1300B47552 /* SessionHighlightingBackgroundLabel.swift */; }; FD71164828E2CE8700B47552 /* SessionCell+AccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164728E2CE8700B47552 /* SessionCell+AccessoryView.swift */; }; - FD71164A28E3EA5B00B47552 /* DismissType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164928E3EA5B00B47552 /* DismissType.swift */; }; FD71164E28E3F8CC00B47552 /* SessionCell+Info.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164D28E3F8CC00B47552 /* SessionCell+Info.swift */; }; FD71165228E410BE00B47552 /* SessionTableSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71165128E410BE00B47552 /* SessionTableSection.swift */; }; FD71165828E436E800B47552 /* Modal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08323399ACF000F5AE3 /* Modal.swift */; }; @@ -1132,7 +1134,6 @@ FDE755182C9BC169002A2623 /* UIAlertAction+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755132C9BC169002A2623 /* UIAlertAction+Utilities.swift */; }; FDE755192C9BC169002A2623 /* UIImage+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755142C9BC169002A2623 /* UIImage+Utilities.swift */; }; FDE7551A2C9BC169002A2623 /* UIApplicationState+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755152C9BC169002A2623 /* UIApplicationState+Utilities.swift */; }; - FDE7551B2C9BC169002A2623 /* UINavigationController+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755162C9BC169002A2623 /* UINavigationController+Utilities.swift */; }; FDE7551C2C9BC169002A2623 /* UIBezierPath+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755172C9BC169002A2623 /* UIBezierPath+Utilities.swift */; }; FDE755202C9BC1A6002A2623 /* CacheConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7551F2C9BC1A6002A2623 /* CacheConfig.swift */; }; FDE755222C9BC1BA002A2623 /* LibSessionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755212C9BC1BA002A2623 /* LibSessionError.swift */; }; @@ -1639,6 +1640,10 @@ 94805EB82EB1E1650055EBBC /* SessionProBottomSheet+Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProBottomSheet+Models.swift"; sourceTree = ""; }; 94805EBA2EB311860055EBBC /* SessionProState+BottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProState+BottomSheet.swift"; sourceTree = ""; }; 94805EBE2EB462C10055EBBC /* TransitionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionType.swift; sourceTree = ""; }; + 94805EC02EB48D860055EBBC /* SessionProState+Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProState+Models.swift"; sourceTree = ""; }; + 94805EC22EB48EC40055EBBC /* SessionProPaymentScreenContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProPaymentScreenContent.swift; sourceTree = ""; }; + 94805EC52EB823B00055EBBC /* DismissType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissType.swift; sourceTree = ""; }; + 94805EC72EB834CD0055EBBC /* UINavigationController+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Utilities.swift"; sourceTree = ""; }; 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableLabel.swift; sourceTree = ""; }; 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadNotificationSettingsViewModelSpec.swift; sourceTree = ""; }; 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Constants+Apple.swift"; sourceTree = ""; }; @@ -1670,7 +1675,6 @@ 94D716812E8FA19D008294EE /* AttributedLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedLabel.swift; sourceTree = ""; }; 94D716852E93394B008294EE /* SessionProBadge+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProBadge+Utilities.swift"; sourceTree = ""; }; 94D716902E9379A4008294EE /* MentionUtilities+Attributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MentionUtilities+Attributes.swift"; sourceTree = ""; }; - 94D955EA2E9CA5DA00DEE66E /* SessionProPaymentScreen+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProPaymentScreen+ViewModel.swift"; sourceTree = ""; }; 94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Localization+Style.swift"; sourceTree = ""; }; A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; A163E8AA16F3F6A90094D68B /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; @@ -2179,7 +2183,6 @@ FD71164328E2CB8A00B47552 /* SessionCell+Accessory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCell+Accessory.swift"; sourceTree = ""; }; FD71164528E2CC1300B47552 /* SessionHighlightingBackgroundLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionHighlightingBackgroundLabel.swift; sourceTree = ""; }; FD71164728E2CE8700B47552 /* SessionCell+AccessoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCell+AccessoryView.swift"; sourceTree = ""; }; - FD71164928E3EA5B00B47552 /* DismissType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissType.swift; sourceTree = ""; }; FD71164D28E3F8CC00B47552 /* SessionCell+Info.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCell+Info.swift"; sourceTree = ""; }; FD71165128E410BE00B47552 /* SessionTableSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTableSection.swift; sourceTree = ""; }; FD71165A28E6DDBC00B47552 /* StyledNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StyledNavigationController.swift; sourceTree = ""; }; @@ -2442,7 +2445,6 @@ FDE755132C9BC169002A2623 /* UIAlertAction+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertAction+Utilities.swift"; sourceTree = ""; }; FDE755142C9BC169002A2623 /* UIImage+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Utilities.swift"; sourceTree = ""; }; FDE755152C9BC169002A2623 /* UIApplicationState+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIApplicationState+Utilities.swift"; sourceTree = ""; }; - FDE755162C9BC169002A2623 /* UINavigationController+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Utilities.swift"; sourceTree = ""; }; FDE755172C9BC169002A2623 /* UIBezierPath+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIBezierPath+Utilities.swift"; sourceTree = ""; }; FDE7551F2C9BC1A6002A2623 /* CacheConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheConfig.swift; sourceTree = ""; }; FDE755212C9BC1BA002A2623 /* LibSessionError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibSessionError.swift; sourceTree = ""; }; @@ -2975,7 +2977,6 @@ 94363E602E6014630004EE43 /* SessionProSettings */ = { isa = PBXGroup; children = ( - 94D955EA2E9CA5DA00DEE66E /* SessionProPaymentScreen+ViewModel.swift */, 94363E612E6014880004EE43 /* SessionProSettingsViewModel.swift */, 94363E692E613AEE0004EE43 /* SessionProSettingsViewModel+Database.swift */, ); @@ -3027,6 +3028,17 @@ path = SessionProBottomSheet; sourceTree = ""; }; + 94805EC42EB8156A0055EBBC /* SessionPro */ = { + isa = PBXGroup; + children = ( + 94805EC22EB48EC40055EBBC /* SessionProPaymentScreenContent.swift */, + 94B6BAF52E30A88800E718BB /* SessionProState.swift */, + 94805EC02EB48D860055EBBC /* SessionProState+Models.swift */, + 94805EBA2EB311860055EBBC /* SessionProState+BottomSheet.swift */, + ); + path = SessionPro; + sourceTree = ""; + }; 94CD96282E1B855E0097754D /* Input View */ = { isa = PBXGroup; children = ( @@ -3470,6 +3482,7 @@ C331FFAE2558FA7700070591 /* Utilities */ = { isa = PBXGroup; children = ( + 94805EC72EB834CD0055EBBC /* UINavigationController+Utilities.swift */, 94B6BB012E3AE85800E718BB /* QRCode.swift */, 94CD962F2E1B88430097754D /* CGRect+Utilities.swift */, FD848B9728422F1A000E298B /* Date+Utilities.swift */, @@ -3795,8 +3808,7 @@ C3BBE0B32554F0D30050F1E3 /* Utilities */ = { isa = PBXGroup; children = ( - 94B6BAF52E30A88800E718BB /* SessionProState.swift */, - 94805EBA2EB311860055EBBC /* SessionProState+BottomSheet.swift */, + 94805EC42EB8156A0055EBBC /* SessionPro */, FD428B1E2B4B758B006D0888 /* AppReadiness.swift */, FDE5218D2E03A06700061B8E /* AttachmentManager.swift */, FDE5219D2E0D0B9800061B8E /* AsyncAccessible.swift */, @@ -4145,7 +4157,6 @@ FDE755152C9BC169002A2623 /* UIApplicationState+Utilities.swift */, FDE755172C9BC169002A2623 /* UIBezierPath+Utilities.swift */, FDE755142C9BC169002A2623 /* UIImage+Utilities.swift */, - FDE755162C9BC169002A2623 /* UINavigationController+Utilities.swift */, FD99D0862D0FA72E005D2E15 /* ThreadSafe.swift */, FD29598C2A43BC0B00888A17 /* Version.swift */, ); @@ -4760,6 +4771,7 @@ FD71163028E2C41900B47552 /* Types */ = { isa = PBXGroup; children = ( + 94805EC52EB823B00055EBBC /* DismissType.swift */, 94805EBE2EB462C10055EBBC /* TransitionType.swift */, FDE6E99729F8E63A00F93C5D /* Accessibility.swift */, FD71163128E2C42A00B47552 /* IconSize.swift */, @@ -4787,7 +4799,6 @@ FD71164128E2C83500B47552 /* Types */ = { isa = PBXGroup; children = ( - FD71164928E3EA5B00B47552 /* DismissType.swift */, FD12A83E2AD63BDF00EEBA0D /* Navigatable.swift */, FD12A8402AD63BEA00EEBA0D /* NavigatableState.swift */, FD12A8422AD63BF600EEBA0D /* ObservableTableSource.swift */, @@ -6403,9 +6414,11 @@ FD3FAB632AEB9A1500DC5421 /* ToastController.swift in Sources */, C331FFE72558FB0000070591 /* SNTextField.swift in Sources */, 94805EB92EB1E16D0055EBBC /* SessionProBottomSheet+Models.swift in Sources */, + 94805EC62EB823B80055EBBC /* DismissType.swift in Sources */, 942256962C23F8DD00C0FDBF /* CompatibleScrollingVStack.swift in Sources */, FD71165B28E6DDBC00B47552 /* StyledNavigationController.swift in Sources */, C331FFE32558FB0000070591 /* TabBar.swift in Sources */, + 94805EC82EB834D40055EBBC /* UINavigationController+Utilities.swift in Sources */, FD37E9D528A1FCE8003AE748 /* Theme+OceanLight.swift in Sources */, FDF848F129406A30007DCAE5 /* Format.swift in Sources */, 94519A952E851BF500F02723 /* SessionProPaymentScreen+UpdatePlan.swift in Sources */, @@ -6810,7 +6823,6 @@ FDF01FAD2A9ECC4200CAF969 /* SingletonConfig.swift in Sources */, FDE755182C9BC169002A2623 /* UIAlertAction+Utilities.swift in Sources */, FD7115F828C8151C00B47552 /* DisposableBarButtonItem.swift in Sources */, - FDE7551B2C9BC169002A2623 /* UINavigationController+Utilities.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6937,6 +6949,7 @@ FD09798D27FD1D8900936362 /* DisappearingMessageConfiguration.swift in Sources */, FD2272732C32911C004D8A6C /* ConfigurationSyncJob.swift in Sources */, FDF0B75A2807F3A3004C14C5 /* MessageSenderError.swift in Sources */, + 94805EC12EB48D910055EBBC /* SessionProState+Models.swift in Sources */, FD245C692850666800B966DD /* ExpirationTimerUpdate.swift in Sources */, FD2272752C32911C004D8A6C /* RetrieveDefaultOpenGroupRoomsJob.swift in Sources */, FD2272712C32911C004D8A6C /* MessageReceiveJob.swift in Sources */, @@ -6970,6 +6983,7 @@ FDD23AE42E458C810057E853 /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */, FD22727B2C32911C004D8A6C /* MessageSendJob.swift in Sources */, FD78E9EE2DD6D32500D55B50 /* ImageDataManager+Singleton.swift in Sources */, + 94805EC32EB48ED50055EBBC /* SessionProPaymentScreenContent.swift in Sources */, C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */, FD981BC42DC304E600564172 /* MessageDeduplication.swift in Sources */, FDF0B74B28061F7A004C14C5 /* InteractionAttachment.swift in Sources */, @@ -7133,7 +7147,6 @@ 45C0DC1E1E69011F00E04C47 /* UIStoryboard+OWS.swift in Sources */, FDE754B02C9B96B4002A2623 /* WebRTCSession.swift in Sources */, B835246E25C38ABF0089A44F /* ConversationVC.swift in Sources */, - 94D955EB2E9CA5E000DEE66E /* SessionProPaymentScreen+ViewModel.swift in Sources */, 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */, 7BBBDC462875600700747E59 /* DocumentTitleViewController.swift in Sources */, FD71163F28E2C82C00B47552 /* SessionHeaderView.swift in Sources */, @@ -7265,7 +7278,6 @@ FDE754B62C9B96BB002A2623 /* WebRTCSession+UI.swift in Sources */, C302093E25DCBF08001F572D /* MentionSelectionView.swift in Sources */, FD71163828E2C50700B47552 /* SessionTableViewModel.swift in Sources */, - FD71164A28E3EA5B00B47552 /* DismissType.swift in Sources */, C328251F25CA3A900062D0A7 /* QuoteView.swift in Sources */, 7B3A39322980D02B002FE4AC /* SessionCarouselView.swift in Sources */, B8EB20F02640F7F000773E52 /* OpenGroupInvitationView.swift in Sources */, diff --git a/Session/Settings/ImagePickerHandler.swift b/Session/Settings/ImagePickerHandler.swift index 24811c715f..c9e91e7f45 100644 --- a/Session/Settings/ImagePickerHandler.swift +++ b/Session/Settings/ImagePickerHandler.swift @@ -3,6 +3,7 @@ import UIKit import UniformTypeIdentifiers import SessionUtilitiesKit +import SessionUIKit class ImagePickerHandler: NSObject, UIImagePickerControllerDelegate & UINavigationControllerDelegate { private let onTransition: (UIViewController, TransitionType) -> Void diff --git a/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift b/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift index 4df8b3c17c..cc200e7c19 100644 --- a/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift +++ b/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift @@ -1048,104 +1048,3 @@ extension SessionProSettingsViewModel { self.transitionToScreen(viewController) } } - -// MARK: - Convenience - -extension SessionProPlan { - func info() -> SessionProPaymentScreenContent.SessionProPlanInfo { - let price: Double = self.variant.price - let pricePerMonth: Double = (self.variant.price / Double(self.variant.duration)) - return .init( - duration: self.variant.duration, - totalPrice: price, - pricePerMonth: pricePerMonth, - discountPercent: self.variant.discountPercent, - titleWithPrice: { - switch self.variant { - case .oneMonth: - return "proPriceOneMonth" - .put(key: "monthly_price", value: pricePerMonth.formatted(format: .currency(decimal: true, withLocalSymbol: true))) - .localized() - case .threeMonths: - return "proPriceThreeMonths" - .put(key: "monthly_price", value: pricePerMonth.formatted(format: .currency(decimal: true, withLocalSymbol: true))) - .localized() - case .twelveMonths: - return "proPriceTwelveMonths" - .put(key: "monthly_price", value: pricePerMonth.formatted(format: .currency(decimal: true, withLocalSymbol: true))) - .localized() - } - }(), - subtitleWithPrice: { - switch self.variant { - case .oneMonth: - return "proBilledMonthly" - .put(key: "price", value: price.formatted(format: .currency(decimal: true, withLocalSymbol: true))) - .localized() - case .threeMonths: - return "proBilledQuarterly" - .put(key: "price", value: price.formatted(format: .currency(decimal: true, withLocalSymbol: true))) - .localized() - case .twelveMonths: - return "proBilledAnnually" - .put(key: "price", value: price.formatted(format: .currency(decimal: true, withLocalSymbol: true))) - .localized() - } - }() - ) - } - - static func from(_ info: SessionProPaymentScreenContent.SessionProPlanInfo) -> SessionProPlan { - let variant: SessionProPlan.Variant = { - switch info.duration { - case 1: return .oneMonth - case 3: return .threeMonths - case 12: return .twelveMonths - default: fatalError("Unhandled SessionProPlan.Variant.Duration case") - } - }() - - return SessionProPlan(variant: variant) - } -} - -extension SessionProPlanState { - func toPaymentFlow() -> SessionProPaymentScreenContent.SessionProPlanPaymentFlow { - switch self { - case .none: - return .purchase - case .active(let currentPlan, let expiredOn, let isAutoRenewing, let originatingPlatform): - return .update( - currentPlan: currentPlan.info(), - expiredOn: expiredOn, - isAutoRenewing: isAutoRenewing, - originatingPlatform: { - switch originatingPlatform { - case .iOS: return .iOS - case .Android: return .Android - } - }() - ) - case .expired(let originatingPlatform): - return .renew( - originatingPlatform: { - switch originatingPlatform { - case .iOS: return .iOS - case .Android: return .Android - } - }() - ) - case .refunding(let originatingPlatform, let requestedAt): - return .refund( - originatingPlatform: { - switch originatingPlatform { - case .iOS: return .iOS - case .Android: return .Android - } - }(), - requestedAt: requestedAt - ) - } - } -} - diff --git a/Session/Settings/SessionProSettings/SessionProPaymentScreen+ViewModel.swift b/SessionMessagingKit/Utilities/SessionPro/SessionProPaymentScreenContent.swift similarity index 86% rename from Session/Settings/SessionProSettings/SessionProPaymentScreen+ViewModel.swift rename to SessionMessagingKit/Utilities/SessionPro/SessionProPaymentScreenContent.swift index a428716bb4..9f9a3fc0f4 100644 --- a/Session/Settings/SessionProSettings/SessionProPaymentScreen+ViewModel.swift +++ b/SessionMessagingKit/Utilities/SessionPro/SessionProPaymentScreenContent.swift @@ -13,7 +13,7 @@ extension SessionProPaymentScreenContent { private var dependencies: Dependencies - init(dependencies: Dependencies, dataModel: DataModel) { + public init(dependencies: Dependencies, dataModel: DataModel) { self.dependencies = dependencies self.dataModel = dataModel } @@ -41,9 +41,5 @@ extension SessionProPaymentScreenContent { } } } - - public func openURL(_ url: URL) { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - } } } diff --git a/SessionMessagingKit/Utilities/SessionProState+BottomSheet.swift b/SessionMessagingKit/Utilities/SessionPro/SessionProState+BottomSheet.swift similarity index 94% rename from SessionMessagingKit/Utilities/SessionProState+BottomSheet.swift rename to SessionMessagingKit/Utilities/SessionPro/SessionProState+BottomSheet.swift index d1a48c69d9..25f7b2e7df 100644 --- a/SessionMessagingKit/Utilities/SessionProState+BottomSheet.swift +++ b/SessionMessagingKit/Utilities/SessionPro/SessionProState+BottomSheet.swift @@ -4,8 +4,9 @@ import SessionUIKit import SessionUtilitiesKit import DifferenceKit -public class SessionProBottomSheetViewModel: SessionProBottomSheetViewModelType { +public class SessionProBottomSheetViewModel: SessionProBottomSheetViewModelType, SessionListScreenContent.NavigatableStateHolder { public let dependencies: Dependencies + public let navigatableState: SessionListScreenContent.NavigatableState = .init() public var title: String = "" public var state: SessionListScreenContent.ListItemDataState = SessionListScreenContent.ListItemDataState() @@ -229,8 +230,7 @@ public class SessionProBottomSheetViewModel: SessionProBottomSheetViewModelType .localizedFormatted() ) case .success: - break -// viewModel?.updateProPlan() + viewModel?.updateProPlan() } } ) @@ -304,6 +304,21 @@ public class SessionProBottomSheetViewModel: SessionProBottomSheetViewModelType return result } + func updateProPlan() { + self.transitionToScreen( + SessionProPaymentScreen( + viewModel: SessionProPaymentScreenContent.ViewModel( + dependencies: dependencies, + dataModel: .init( + flow: dependencies[singleton: .sessionProState].sessionProStateSubject.value.toPaymentFlow(), + plans: dependencies[singleton: .sessionProState].sessionProPlans.map { $0.info() } + ) + ) + ), + transitionType: .push + ) + } + public func showLoadingModal(title: String, description: String) { } diff --git a/SessionMessagingKit/Utilities/SessionPro/SessionProState+Models.swift b/SessionMessagingKit/Utilities/SessionPro/SessionProState+Models.swift new file mode 100644 index 0000000000..985eeb2e74 --- /dev/null +++ b/SessionMessagingKit/Utilities/SessionPro/SessionProState+Models.swift @@ -0,0 +1,103 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import SessionUIKit +import SessionUtilitiesKit +import Combine + +public extension SessionProPlanState { + func toPaymentFlow() -> SessionProPaymentScreenContent.SessionProPlanPaymentFlow { + switch self { + case .none: + return .purchase + case .active(let currentPlan, let expiredOn, let isAutoRenewing, let originatingPlatform): + return .update( + currentPlan: currentPlan.info(), + expiredOn: expiredOn, + isAutoRenewing: isAutoRenewing, + originatingPlatform: { + switch originatingPlatform { + case .iOS: return .iOS + case .Android: return .Android + } + }() + ) + case .expired(let originatingPlatform): + return .renew( + originatingPlatform: { + switch originatingPlatform { + case .iOS: return .iOS + case .Android: return .Android + } + }() + ) + case .refunding(let originatingPlatform, let requestedAt): + return .refund( + originatingPlatform: { + switch originatingPlatform { + case .iOS: return .iOS + case .Android: return .Android + } + }(), + requestedAt: requestedAt + ) + } + } +} + +public extension SessionProPlan { + func info() -> SessionProPaymentScreenContent.SessionProPlanInfo { + let price: Double = self.variant.price + let pricePerMonth: Double = (self.variant.price / Double(self.variant.duration)) + return .init( + duration: self.variant.duration, + totalPrice: price, + pricePerMonth: pricePerMonth, + discountPercent: self.variant.discountPercent, + titleWithPrice: { + switch self.variant { + case .oneMonth: + return "proPriceOneMonth" + .put(key: "monthly_price", value: pricePerMonth.formatted(format: .currency(decimal: true, withLocalSymbol: true))) + .localized() + case .threeMonths: + return "proPriceThreeMonths" + .put(key: "monthly_price", value: pricePerMonth.formatted(format: .currency(decimal: true, withLocalSymbol: true))) + .localized() + case .twelveMonths: + return "proPriceTwelveMonths" + .put(key: "monthly_price", value: pricePerMonth.formatted(format: .currency(decimal: true, withLocalSymbol: true))) + .localized() + } + }(), + subtitleWithPrice: { + switch self.variant { + case .oneMonth: + return "proBilledMonthly" + .put(key: "price", value: price.formatted(format: .currency(decimal: true, withLocalSymbol: true))) + .localized() + case .threeMonths: + return "proBilledQuarterly" + .put(key: "price", value: price.formatted(format: .currency(decimal: true, withLocalSymbol: true))) + .localized() + case .twelveMonths: + return "proBilledAnnually" + .put(key: "price", value: price.formatted(format: .currency(decimal: true, withLocalSymbol: true))) + .localized() + } + }() + ) + } + + static func from(_ info: SessionProPaymentScreenContent.SessionProPlanInfo) -> SessionProPlan { + let variant: SessionProPlan.Variant = { + switch info.duration { + case 1: return .oneMonth + case 3: return .threeMonths + case 12: return .twelveMonths + default: fatalError("Unhandled SessionProPlan.Variant.Duration case") + } + }() + + return SessionProPlan(variant: variant) + } +} diff --git a/SessionMessagingKit/Utilities/SessionProState.swift b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift similarity index 93% rename from SessionMessagingKit/Utilities/SessionProState.swift rename to SessionMessagingKit/Utilities/SessionPro/SessionProState.swift index a61ecd8d7b..66a72b7612 100644 --- a/SessionMessagingKit/Utilities/SessionProState.swift +++ b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift @@ -88,14 +88,7 @@ public class SessionProState: SessionProManagerType, ProfilePictureAnimationMana guard case .active(let currentPlan, let expiredOn, _, let originatingPlatform) = self.sessionProStateSubject.value else { return } - self.sessionProStateSubject.send( - SessionProPlanState.active( - currentPlan: currentPlan, - expiredOn: expiredOn, - isAutoRenewing: false, - originatingPlatform: originatingPlatform - ) - ) + self.sessionProStateSubject.send(.none) self.shouldAnimateImageSubject.send(false) completion?(true) } @@ -179,9 +172,10 @@ extension SessionProState: SessionProCTAManagerType { openUrl: ((URL) -> Void)?, presenting: ((UIViewController) -> Void)? ) { + let viewModel = SessionProBottomSheetViewModel(using: dependencies) let sessionProBottomSheet: BottomSheetHostingViewController = BottomSheetHostingViewController( - bottomSheet: BottomSheet(hasCloseButton: true) { [dependencies] in - SessionListScreen(viewModel: SessionProBottomSheetViewModel(using: dependencies)) + bottomSheet: BottomSheet(hasCloseButton: true) { + SessionListScreen(viewModel: viewModel) } ) presenting?(sessionProBottomSheet) diff --git a/SessionUIKit/Components/SwiftUI/BottomSheet.swift b/SessionUIKit/Components/SwiftUI/BottomSheet.swift index 85ddff14d7..d31675922e 100644 --- a/SessionUIKit/Components/SwiftUI/BottomSheet.swift +++ b/SessionUIKit/Components/SwiftUI/BottomSheet.swift @@ -6,10 +6,10 @@ import Combine public struct BottomSheet: View where Content: View { @EnvironmentObject var host: HostWrapper - let navigatableState: BottomSheetNavigatableState + @State private var disposables: Set = Set() + let hasCloseButton: Bool let content: () -> Content - private var disposables: Set = Set() let cornerRadius: CGFloat = 11 let shadowRadius: CGFloat = 10 @@ -19,14 +19,11 @@ public struct BottomSheet: View where Content: View { @State private var contentHeight: CGFloat = 80 public init( - navigatableState: BottomSheetNavigatableState, hasCloseButton: Bool, content: @escaping () -> Content) { - self.navigatableState = navigatableState self.hasCloseButton = hasCloseButton self.content = content - navigatableState.setupBindings(viewController: host.controller, disposables: &disposables) } public var body: some View { @@ -39,7 +36,11 @@ public struct BottomSheet: View where Content: View { // Bottom Sheet ZStack(alignment: .topTrailing) { - content() + NavigationView { + content() + .navigationBarHidden(true) + } + .navigationViewStyle(.stack) if hasCloseButton { Button { @@ -87,7 +88,7 @@ public struct BottomSheet: View where Content: View { } } -// MARK: - ModalHostingViewController +// MARK: - BottomSheetHostingViewController open class BottomSheetHostingViewController: UIHostingController>> where Content: View { public init(bottomSheet: Content) { @@ -113,61 +114,3 @@ open class BottomSheetHostingViewController: UIHostingController - - // MARK: - Internal Variables - - fileprivate let _transitionToScreen: PassthroughSubject<(UIViewController, TransitionType), Never> = PassthroughSubject() - - // MARK: - Initialization - - init() { - self.transitionToScreen = _transitionToScreen - .subscribe(on: DispatchQueue.main) - .eraseToAnyPublisher() - } - - // MARK: - Functions - - public func setupBindings( - viewController: UIViewController?, - disposables: inout Set - ) { - self.transitionToScreen - .receive(on: DispatchQueue.main) - .sink { [weak viewController] targetViewController, transitionType in - switch transitionType { - case .push: - viewController?.navigationController?.pushViewController(targetViewController, animated: true) - - case .present: - let presenter: UIViewController? = (viewController?.presentedViewController ?? viewController) - - if UIDevice.current.isIPad { - targetViewController.popoverPresentationController?.permittedArrowDirections = [] - targetViewController.popoverPresentationController?.sourceView = presenter?.view - targetViewController.popoverPresentationController?.sourceRect = (presenter?.view.bounds ?? UIScreen.main.bounds) - } - - presenter?.present(targetViewController, animated: true) - } - } - .store(in: &disposables) - } -} diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Models.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Models.swift index e9f22821e6..cca5678117 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Models.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Models.swift @@ -147,7 +147,6 @@ public extension SessionProPaymentScreenContent { func purchase(planInfo: SessionProPlanInfo, success: (() -> Void)?, failure: (() -> Void)?) func cancelPro(success: (() -> Void)?, failure: (() -> Void)?) - func openURL(_ url: URL) } } diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift index 60b2677061..40786c3a22 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift @@ -251,7 +251,9 @@ public struct SessionProPaymentScreen: View { Constants.session_pro_privacy_url ], openURL: { url in - viewModel.openURL(url) + if let extensionContext = self.host.controller?.extensionContext { + extensionContext.open(url, completionHandler: nil) + } } ) ) @@ -274,7 +276,11 @@ public struct SessionProPaymentScreen: View { confirmStyle: .danger, cancelTitle: "urlCopy".localized(), cancelStyle: .alert_text, - onConfirm: { _ in viewModel.openURL(url) }, + onConfirm: { _ in + if let extensionContext = self.host.controller?.extensionContext { + extensionContext.open(url, completionHandler: nil) + } + }, onCancel: { modal in UIPasteboard.general.string = url.absoluteString modal.close() diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPlanUpdatedScreen.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPlanUpdatedScreen.swift index 6e8ce5dc83..3a2ad918c6 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPlanUpdatedScreen.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPlanUpdatedScreen.swift @@ -11,7 +11,7 @@ public struct SessionProPlanUpdatedScreen: View { var dismissButtonTitle: String { switch flow { case .purchase, .renew: - "Start Using Pro" + "proStartUsing".put(key: "pro", value: Constants.pro).localized() case .update: "theReturn".localized() default: @@ -20,7 +20,7 @@ public struct SessionProPlanUpdatedScreen: View { } var desription: ThemedAttributedString { switch flow { - case .update(let currentPlan, let expiredOn, let isAutoRenewing, let originatingPlatform): + case .update(_, let expiredOn, _, _): "proAllSetDescription" .put(key: "app_pro", value: Constants.app_pro) .put(key: "pro", value: Constants.pro) @@ -31,6 +31,11 @@ public struct SessionProPlanUpdatedScreen: View { .put(key: "app_pro", value: Constants.app_pro) .put(key: "network_name", value: Constants.network_name) .localizedFormatted(Fonts.Body.baseRegular) + case .purchase: + "proUpgraded" + .put(key: "app_pro", value: Constants.app_pro) + .put(key: "network_name", value: Constants.network_name) + .localizedFormatted(Fonts.Body.baseRegular) default: fatalError("Unexpected case \(flow)") } } diff --git a/SessionUIKit/Screens/Shared/SessionListScreen+Models.swift b/SessionUIKit/Screens/Shared/SessionListScreen+Models.swift index 7f8ad0a008..c767c162cf 100644 --- a/SessionUIKit/Screens/Shared/SessionListScreen+Models.swift +++ b/SessionUIKit/Screens/Shared/SessionListScreen+Models.swift @@ -1,12 +1,112 @@ // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. import Foundation +import SwiftUI +import Combine public enum SessionListScreenContent {} +// MARK: - ViewModelType + public extension SessionListScreenContent { protocol ViewModelType: ObservableObject, SectionedListItemData { var title: String { get } var state: ListItemDataState { get } } } + +// MARK: - Navigatable + +public extension SessionListScreenContent { + struct NavigationDestination: Identifiable { + public let id = UUID() + public let view: AnyView + + public init(_ view: V) { + self.view = AnyView(view) + } + } + + protocol NavigatableStateHolder { + var navigatableState: NavigatableState { get } + } + + struct NavigatableState { + let transitionToScreen: AnyPublisher<(NavigationDestination, TransitionType), Never> + let dismissScreen: AnyPublisher<(DismissType, (() -> Void)?), Never> + + // MARK: - Internal Variables + + fileprivate let _transitionToScreen: PassthroughSubject<(NavigationDestination, TransitionType), Never> = PassthroughSubject() + fileprivate let _dismissScreen: PassthroughSubject<(DismissType, (() -> Void)?), Never> = PassthroughSubject() + + // MARK: - Initialization + + public init() { + self.transitionToScreen = _transitionToScreen.eraseToAnyPublisher() + self.dismissScreen = _dismissScreen.eraseToAnyPublisher() + } + + // MARK: - Functions + + public func setupBindings( + viewController: UIViewController, + disposables: inout Set + ) { + self.transitionToScreen + .receive(on: DispatchQueue.main) + .sink { [weak viewController] destination, transitionType in + let targetViewController = SessionHostingViewController(rootView: destination.view) + + switch transitionType { + case .push: + viewController?.navigationController?.pushViewController(targetViewController, animated: true) + + case .present: + let presenter: UIViewController? = (viewController?.presentedViewController ?? viewController) + + if UIDevice.current.isIPad { + targetViewController.popoverPresentationController?.permittedArrowDirections = [] + targetViewController.popoverPresentationController?.sourceView = presenter?.view + targetViewController.popoverPresentationController?.sourceRect = (presenter?.view.bounds ?? UIScreen.main.bounds) + } + + presenter?.present(targetViewController, animated: true) + } + } + .store(in: &disposables) + + self.dismissScreen + .receive(on: DispatchQueue.main) + .sink { [weak viewController] dismissType, completion in + switch dismissType { + case .auto: + guard + let viewController: UIViewController = viewController, + (viewController.navigationController?.viewControllers.firstIndex(of: viewController) ?? 0) > 0 + else { + viewController?.dismiss(animated: true, completion: completion) + return + } + + viewController.navigationController?.popViewController(animated: true, completion: completion) + + case .dismiss: viewController?.dismiss(animated: true, completion: completion) + case .pop: viewController?.navigationController?.popViewController(animated: true, completion: completion) + case .popToRoot: viewController?.navigationController?.popToRootViewController(animated: true, completion: completion) + } + } + .store(in: &disposables) + } + } +} + +public extension SessionListScreenContent.NavigatableStateHolder { + func dismissScreen(type: DismissType = .auto, completion: (() -> Void)? = nil) { + navigatableState._dismissScreen.send((type, completion)) + } + + func transitionToScreen(_ view: V, transitionType: TransitionType = .push) { + navigatableState._transitionToScreen.send((SessionListScreenContent.NavigationDestination(view), transitionType)) + } +} diff --git a/SessionUIKit/Screens/Shared/SessionListScreen.swift b/SessionUIKit/Screens/Shared/SessionListScreen.swift index 71be3a36b8..7d071b1aa6 100644 --- a/SessionUIKit/Screens/Shared/SessionListScreen.swift +++ b/SessionUIKit/Screens/Shared/SessionListScreen.swift @@ -1,6 +1,7 @@ // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. import SwiftUI +import Combine public struct SessionListScreen: View { @EnvironmentObject var host: HostWrapper @@ -18,12 +19,63 @@ public struct SessionListScreen { + navigatableState?.transitionToScreen ?? Empty().eraseToAnyPublisher() } + @ViewBuilder + private var destinationView: some View { + if let destination = navigationDestination { + destination.view + } else { + EmptyView() + } + } + + // MARK: - Body + public var body: some View { + ZStack { + listContent + + // Hidden NavigationLink for publisher-driven navigation + NavigationLink( + destination: destinationView, + isActive: $isNavigationActive + ) { + EmptyView() + } + .hidden() + } + .onReceive(navigationPublisher) { destination, transitionType in + // Only handle push transitions in SwiftUI + // Present transitions are handled by UIKit in setupBindings + if transitionType == .push { + navigationDestination = destination + isNavigationActive = true + } + } + } + + private var listContent: some View { List { ForEach(state.listItemData, id: \.model) { section in Section { @@ -161,4 +213,3 @@ public struct SessionListScreen Date: Wed, 5 Nov 2025 17:06:39 +1100 Subject: [PATCH 09/60] fix ui issues in bottom sheet --- .../SessionProSettingsViewModel.swift | 12 +- .../SessionProPaymentScreenContent.swift | 4 +- .../SessionProState+BottomSheet.swift | 6 +- .../Components/SwiftUI/BottomSheet.swift | 62 ++-- .../SessionProPaymentScreen+Models.swift | 1 + .../SessionProPaymentScreen.swift | 328 ++++++++++-------- .../SessionProPlanUpdatedScreen.swift | 14 +- .../SessionListScreen+ListItemView.swift | 174 +++++++--- 8 files changed, 355 insertions(+), 246 deletions(-) diff --git a/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift b/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift index cc200e7c19..bfdb737f9d 100644 --- a/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift +++ b/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift @@ -231,12 +231,13 @@ public class SessionProSettingsViewModel: SessionListScreenContent.ViewModelType id: .logoWithPro, variant: .logoWithPro( info: .init( - style:{ + themeStyle:{ switch state.currentProPlanState { case .expired: .disabled default: .normal } }(), + glowingBackgroundStyle: .base, state: { switch state.loadingState { case .loading: @@ -951,7 +952,8 @@ extension SessionProSettingsViewModel { dataModel: .init( flow: dependencies[singleton: .sessionProState].sessionProStateSubject.value.toPaymentFlow(), plans: dependencies[singleton: .sessionProState].sessionProPlans.map { $0.info() } - ) + ), + isFromBottomSheet: false ) ) ) @@ -1018,7 +1020,8 @@ extension SessionProSettingsViewModel { }() ), plans: dependencies[singleton: .sessionProState].sessionProPlans.map { $0.info() } - ) + ), + isFromBottomSheet: false ) ) ) @@ -1041,7 +1044,8 @@ extension SessionProSettingsViewModel { requestedAt: nil ), plans: dependencies[singleton: .sessionProState].sessionProPlans.map { $0.info() } - ) + ), + isFromBottomSheet: false ) ) ) diff --git a/SessionMessagingKit/Utilities/SessionPro/SessionProPaymentScreenContent.swift b/SessionMessagingKit/Utilities/SessionPro/SessionProPaymentScreenContent.swift index 9f9a3fc0f4..b87266e35d 100644 --- a/SessionMessagingKit/Utilities/SessionPro/SessionProPaymentScreenContent.swift +++ b/SessionMessagingKit/Utilities/SessionPro/SessionProPaymentScreenContent.swift @@ -10,12 +10,14 @@ extension SessionProPaymentScreenContent { public var dataModel: DataModel public var isRefreshing: Bool = false public var errorString: String? + public var isFromBottomSheet: Bool private var dependencies: Dependencies - public init(dependencies: Dependencies, dataModel: DataModel) { + public init(dependencies: Dependencies, dataModel: DataModel, isFromBottomSheet: Bool) { self.dependencies = dependencies self.dataModel = dataModel + self.isFromBottomSheet = isFromBottomSheet } public func purchase(planInfo: SessionProPlanInfo, success: (() -> Void)?, failure: (() -> Void)?) { diff --git a/SessionMessagingKit/Utilities/SessionPro/SessionProState+BottomSheet.swift b/SessionMessagingKit/Utilities/SessionPro/SessionProState+BottomSheet.swift index 25f7b2e7df..52463affe6 100644 --- a/SessionMessagingKit/Utilities/SessionPro/SessionProState+BottomSheet.swift +++ b/SessionMessagingKit/Utilities/SessionPro/SessionProState+BottomSheet.swift @@ -123,7 +123,8 @@ public class SessionProBottomSheetViewModel: SessionProBottomSheetViewModelType, id: .logoWithPro, variant: .logoWithPro( info: .init( - style:.normal, + themeStyle:.normal, + glowingBackgroundStyle: .base, state: { switch state.loadingState { case .loading: @@ -312,7 +313,8 @@ public class SessionProBottomSheetViewModel: SessionProBottomSheetViewModelType, dataModel: .init( flow: dependencies[singleton: .sessionProState].sessionProStateSubject.value.toPaymentFlow(), plans: dependencies[singleton: .sessionProState].sessionProPlans.map { $0.info() } - ) + ), + isFromBottomSheet: true ) ), transitionType: .push diff --git a/SessionUIKit/Components/SwiftUI/BottomSheet.swift b/SessionUIKit/Components/SwiftUI/BottomSheet.swift index d31675922e..6c25019c84 100644 --- a/SessionUIKit/Components/SwiftUI/BottomSheet.swift +++ b/SessionUIKit/Components/SwiftUI/BottomSheet.swift @@ -16,7 +16,7 @@ public struct BottomSheet: View where Content: View { let shadowOpacity: Double = 0.4 @State private var show: Bool = true - @State private var contentHeight: CGFloat = 80 + @State private var topPadding: CGFloat = 80 public init( hasCloseButton: Bool, @@ -34,34 +34,46 @@ public struct BottomSheet: View where Content: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .ignoresSafeArea() - // Bottom Sheet - ZStack(alignment: .topTrailing) { - NavigationView { - content() - .navigationBarHidden(true) - } - .navigationViewStyle(.stack) + VStack(spacing: Values.verySmallSpacing) { + Capsule() + .fill(themeColor: .value(.textPrimary, alpha: 0.8)) + .frame(width: 35, height: 3) - if hasCloseButton { - Button { - close() - } label: { - AttributedText(Lucide.Icon.x.attributedString(size: 20)) - .font(.system(size: 20)) - .foregroundColor(themeColor: .textPrimary) + // Bottom Sheet + ZStack(alignment: .topTrailing) { + GeometryReader { geo in + NavigationView { + content() + .padding(.top, 44) + } + .navigationViewStyle(.stack) + .onAppear { + let screenHeight = UIScreen.main.bounds.height + let bottomSafeInset = host.controller?.view.safeAreaInsets.bottom ?? 0 + topPadding = screenHeight - bottomSafeInset - geo.size.height - 44 - Values.largeSpacing + } + } + + if hasCloseButton { + Button { + close() + } label: { + AttributedText(Lucide.Icon.x.attributedString(size: 20)) + .font(.system(size: 20)) + .foregroundColor(themeColor: .textPrimary) + } + .frame(width: 24, height: 24) + .padding(Values.mediumSmallSpacing) } - .frame(width: 24, height: 24) - .padding(Values.mediumSmallSpacing) } + .backgroundColor(themeColor: .backgroundPrimary) + .cornerRadius(cornerRadius, corners: [.topLeft, .topRight]) + .frame( + maxWidth: .infinity, + alignment: .topTrailing + ) } - .backgroundColor(themeColor: .backgroundPrimary) - .cornerRadius(cornerRadius, corners: [.topLeft, .topRight]) - .shadow(color: Color.black.opacity(shadowOpacity), radius: shadowRadius) - .frame( - maxWidth: .infinity, - alignment: .topTrailing - ) - .padding(.top, 80) + .padding(.top, topPadding) .transition(.move(edge: .bottom).combined(with: .opacity)) .animation(.spring(), value: show) } diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Models.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Models.swift index cca5678117..916c42b199 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Models.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Models.swift @@ -144,6 +144,7 @@ public extension SessionProPaymentScreenContent { var dataModel: DataModel { get set } var isRefreshing: Bool { get set } var errorString: String? { get set } + var isFromBottomSheet: Bool { get } func purchase(planInfo: SessionProPlanInfo, success: (() -> Void)?, failure: (() -> Void)?) func cancelPro(success: (() -> Void)?, failure: (() -> Void)?) diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift index 40786c3a22..0fc066121e 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift @@ -5,6 +5,7 @@ import Lucide public struct SessionProPaymentScreen: View { @EnvironmentObject var host: HostWrapper + @State private var isNavigationActive: Bool = false @State var currentSelection: Int @State private var isShowingTooltip: Bool = false @@ -32,116 +33,29 @@ public struct SessionProPaymentScreen: View { public var body: some View { GeometryReader { geometry in ScrollView(.vertical, showsIndicators: false) { - VStack(spacing: Values.mediumSmallSpacing) { - ListItemLogoWithPro( - info: .init( - style: { - switch viewModel.dataModel.flow { - case .refund, .cancel: return .disabled - default: return .normal - } - }(), - state: .success, - description: viewModel.dataModel.flow.description + ZStack(alignment: .topLeading) { + content + .padding(.horizontal, Values.largeSpacing) + .frame( + maxWidth: .infinity, + minHeight: geometry.size.height ) - ) - if case .purchase = viewModel.dataModel.flow { - SessionProPlanPurchaseContent( - currentSelection: $currentSelection, - isShowingTooltip: $isShowingTooltip, - suppressUntil: $suppressUntil, - currentPlan: nil, - sessionProPlans: viewModel.dataModel.plans, - actionButtonTitle: "Upgrade", - purchaseAction: { updatePlan() }, - openTosPrivacyAction: { openTosPrivacy() } - ) - } else if case .renew(let originatingPlatform) = viewModel.dataModel.flow { - if viewModel.dataModel.plans.isEmpty { - RenewPlanNoBillingAccessContent( - originatingPlatform: originatingPlatform, - openPlatformStoreWebsiteAction: { openPlatformStoreWebsite() } - ) - } else { - SessionProPlanPurchaseContent( - currentSelection: $currentSelection, - isShowingTooltip: $isShowingTooltip, - suppressUntil: $suppressUntil, - currentPlan: nil, - sessionProPlans: viewModel.dataModel.plans, - actionButtonTitle: "renew".localized(), - purchaseAction: { updatePlan() }, - openTosPrivacyAction: { openTosPrivacy() } - ) - } - } else if case .update(let currentPlan, let expiredOn, let isAutoRenewing, let originatingPlatform) = viewModel.dataModel.flow { - if originatingPlatform == .iOS { - SessionProPlanPurchaseContent( - currentSelection: $currentSelection, - isShowingTooltip: $isShowingTooltip, - suppressUntil: $suppressUntil, - currentPlan: currentPlan, - sessionProPlans: viewModel.dataModel.plans, - actionButtonTitle: "updateAccess".put(key: "pro", value: Constants.pro).localized(), - purchaseAction: { updatePlan() }, - openTosPrivacyAction: { openTosPrivacy() } - ) - } else { - UpdatePlanNonOriginatingPlatformContent( - currentPlan: currentPlan, - currentPlanExpiredOn: expiredOn, - isAutoRenewing: isAutoRenewing, - originatingPlatform: originatingPlatform, - openPlatformStoreWebsiteAction: { openPlatformStoreWebsite() } - ) - } - } else if case .refund(let originatingPlatform, let requestedAt) = viewModel.dataModel.flow { - if originatingPlatform == .iOS { - RequestRefundOriginatingPlatformContent( - requestRefundAction: {} - ) - } else { - RequestRefundNonOriginatingPlatformContent( - originatingPlatform: originatingPlatform, - requestedAt: requestedAt, - openPlatformStoreWebsiteAction: { openPlatformStoreWebsite() } - ) - } - } else if case .cancel(let originatingPlatform) = viewModel.dataModel.flow { - if originatingPlatform == .iOS { - CancelPlanOriginatingPlatformContent( - cancelPlanAction: { - viewModel.cancelPro( - success: { - host.controller?.navigationController?.popViewController(animated: true) - }, - failure: { - - } - ) - } - ) - } else { - CancelPlanNonOriginatingPlatformContent( - originatingPlatform: originatingPlatform, - openPlatformStoreWebsiteAction: { openPlatformStoreWebsite() } - ) + .onAnyInteraction(scrollCoordinateSpaceName: coordinateSpaceName) { + guard self.isShowingTooltip else { return } + suppressUntil = Date().addingTimeInterval(0.2) + withAnimation(.spring()) { + self.isShowingTooltip = false + } } - } - Spacer() - } - .padding(.horizontal, Values.largeSpacing) - .frame( - maxWidth: .infinity, - minHeight: geometry.size.height - ) - .onAnyInteraction(scrollCoordinateSpaceName: coordinateSpaceName) { - guard self.isShowingTooltip else { return } - suppressUntil = Date().addingTimeInterval(0.2) - withAnimation(.spring()) { - self.isShowingTooltip = false + // Hidden NavigationLink for publisher-driven navigation + NavigationLink( + destination: destinationView, + isActive: $isNavigationActive + ) { + EmptyView() } + .hidden() } } .coordinateSpace(name: coordinateSpaceName) @@ -174,64 +88,172 @@ public struct SessionProPaymentScreen: View { } } - private func updatePlan() { - let updatedPlan = viewModel.dataModel.plans[currentSelection] - if - case .update(let currentPlan, let expiredOn, let isAutoRenewing, _) = viewModel.dataModel.flow, - let updatedPlanExpiredOn = Calendar.current.date(byAdding: .month, value: updatedPlan.duration, to: expiredOn) - { - let confirmationModal = ConfirmationModal( + private var destinationView: some View { + SessionProPlanUpdatedScreen( + flow: self.viewModel.dataModel.flow, + expiredOn: nil + ) + } + + private var content: some View { + VStack(spacing: Values.mediumSmallSpacing) { + ListItemLogoWithPro( info: .init( - title: "updateAccess" - .put(key: "pro", value: Constants.pro) - .localized(), - body: .attributedText( - isAutoRenewing ? - "proUpdateAccessDescription" - .put(key: "current_plan_length", value: currentPlan.durationString) - .put(key: "selected_plan_length", value: updatedPlan.durationString) - .put(key: "selected_plan_length_singular", value: updatedPlan.durationStringSingular) - .put(key: "date", value: expiredOn.formatted("MMM dd, yyyy")) - .put(key: "pro", value: Constants.pro) - .localizedFormatted(Fonts.Body.largeRegular) : - "proUpdateAccessExpireDescription" - .put(key: "date", value: expiredOn.formatted("MMM dd, yyyy")) - .put(key: "selected_plan_length", value: updatedPlan.durationString) - .put(key: "pro", value: Constants.pro) - .localizedFormatted(Fonts.Body.largeRegular), - scrollMode: .never - ), - confirmTitle: "update".localized(), - onConfirm: { _ in - self.viewModel.purchase( - planInfo: updatedPlan, - success: { onPaymentSuccess(expiredOn: updatedPlanExpiredOn) }, - failure: { - - } - ) - } + themeStyle: { + switch viewModel.dataModel.flow { + case .refund, .cancel: return .disabled + default: return .normal + } + }(), + glowingBackgroundStyle: .base, + state: .success, + description: viewModel.dataModel.flow.description ) ) - self.host.controller?.present(confirmationModal, animated: true) + if case .purchase = viewModel.dataModel.flow { + SessionProPlanPurchaseContent( + currentSelection: $currentSelection, + isShowingTooltip: $isShowingTooltip, + suppressUntil: $suppressUntil, + currentPlan: nil, + sessionProPlans: viewModel.dataModel.plans, + actionButtonTitle: "upgrade".localized(), + purchaseAction: { updatePlan() }, + openTosPrivacyAction: { openTosPrivacy() } + ) + } else if case .renew(let originatingPlatform) = viewModel.dataModel.flow { + if viewModel.dataModel.plans.isEmpty { + RenewPlanNoBillingAccessContent( + originatingPlatform: originatingPlatform, + openPlatformStoreWebsiteAction: { openPlatformStoreWebsite() } + ) + } else { + SessionProPlanPurchaseContent( + currentSelection: $currentSelection, + isShowingTooltip: $isShowingTooltip, + suppressUntil: $suppressUntil, + currentPlan: nil, + sessionProPlans: viewModel.dataModel.plans, + actionButtonTitle: "renew".localized(), + purchaseAction: { updatePlan() }, + openTosPrivacyAction: { openTosPrivacy() } + ) + } + } else if case .update(let currentPlan, let expiredOn, let isAutoRenewing, let originatingPlatform) = viewModel.dataModel.flow { + if originatingPlatform == .iOS { + SessionProPlanPurchaseContent( + currentSelection: $currentSelection, + isShowingTooltip: $isShowingTooltip, + suppressUntil: $suppressUntil, + currentPlan: currentPlan, + sessionProPlans: viewModel.dataModel.plans, + actionButtonTitle: "updateAccess".put(key: "pro", value: Constants.pro).localized(), + purchaseAction: { updatePlan() }, + openTosPrivacyAction: { openTosPrivacy() } + ) + } else { + UpdatePlanNonOriginatingPlatformContent( + currentPlan: currentPlan, + currentPlanExpiredOn: expiredOn, + isAutoRenewing: isAutoRenewing, + originatingPlatform: originatingPlatform, + openPlatformStoreWebsiteAction: { openPlatformStoreWebsite() } + ) + } + } else if case .refund(let originatingPlatform, let requestedAt) = viewModel.dataModel.flow { + if originatingPlatform == .iOS { + RequestRefundOriginatingPlatformContent( + requestRefundAction: {} + ) + } else { + RequestRefundNonOriginatingPlatformContent( + originatingPlatform: originatingPlatform, + requestedAt: requestedAt, + openPlatformStoreWebsiteAction: { openPlatformStoreWebsite() } + ) + } + } else if case .cancel(let originatingPlatform) = viewModel.dataModel.flow { + if originatingPlatform == .iOS { + CancelPlanOriginatingPlatformContent( + cancelPlanAction: { + viewModel.cancelPro( + success: { + host.controller?.navigationController?.popViewController(animated: true) + }, + failure: { + + } + ) + } + ) + } else { + CancelPlanNonOriginatingPlatformContent( + originatingPlatform: originatingPlatform, + openPlatformStoreWebsiteAction: { openPlatformStoreWebsite() } + ) + } + } } - + } + + private func updatePlan() { + let updatedPlan = viewModel.dataModel.plans[currentSelection] switch viewModel.dataModel.flow { - case .purchase, .renew: - if let updatedPlanExpiredOn = Calendar.current.date(byAdding: .month, value: updatedPlan.duration, to: Date()) { - self.viewModel.purchase( - planInfo: updatedPlan, - success: { onPaymentSuccess(expiredOn: updatedPlanExpiredOn) }, - failure: { - - } + case .update(let currentPlan, let expiredOn, let isAutoRenewing, _): + if let updatedPlanExpiredOn = Calendar.current.date(byAdding: .month, value: updatedPlan.duration, to: expiredOn) { + let confirmationModal = ConfirmationModal( + info: .init( + title: "updateAccess" + .put(key: "pro", value: Constants.pro) + .localized(), + body: .attributedText( + isAutoRenewing ? + "proUpdateAccessDescription" + .put(key: "current_plan_length", value: currentPlan.durationString) + .put(key: "selected_plan_length", value: updatedPlan.durationString) + .put(key: "selected_plan_length_singular", value: updatedPlan.durationStringSingular) + .put(key: "date", value: expiredOn.formatted("MMM dd, yyyy")) + .put(key: "pro", value: Constants.pro) + .localizedFormatted(Fonts.Body.largeRegular) : + "proUpdateAccessExpireDescription" + .put(key: "date", value: expiredOn.formatted("MMM dd, yyyy")) + .put(key: "selected_plan_length", value: updatedPlan.durationString) + .put(key: "pro", value: Constants.pro) + .localizedFormatted(Fonts.Body.largeRegular), + scrollMode: .never + ), + confirmTitle: "update".localized(), + onConfirm: { _ in + self.viewModel.purchase( + planInfo: updatedPlan, + success: { onPaymentSuccess(expiredOn: updatedPlanExpiredOn) }, + failure: { + + } + ) + } + ) ) + self.host.controller?.present(confirmationModal, animated: true) } + case .purchase, .renew: + self.viewModel.purchase( + planInfo: updatedPlan, + success: { onPaymentSuccess(expiredOn: nil) }, + failure: { + + } + ) default: break } } - private func onPaymentSuccess(expiredOn: Date) { + private func onPaymentSuccess(expiredOn: Date?) { + guard !self.viewModel.isFromBottomSheet else { + isNavigationActive = true + return + } + let viewController: SessionHostingViewController = SessionHostingViewController( rootView: SessionProPlanUpdatedScreen( flow: self.viewModel.dataModel.flow, diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPlanUpdatedScreen.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPlanUpdatedScreen.swift index 3a2ad918c6..28b9c1f0e6 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPlanUpdatedScreen.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPlanUpdatedScreen.swift @@ -6,12 +6,12 @@ import Lucide public struct SessionProPlanUpdatedScreen: View { @EnvironmentObject var host: HostWrapper let flow: SessionProPaymentScreenContent.SessionProPlanPaymentFlow - let expiredOn: Date + let expiredOn: Date? var blurSize: CGFloat { UIScreen.main.bounds.width - 2 * Values.mediumSpacing } var dismissButtonTitle: String { switch flow { case .purchase, .renew: - "proStartUsing".put(key: "pro", value: Constants.pro).localized() + "proStartUsing".put(key: "pro", value: Constants.pro).localized() case .update: "theReturn".localized() default: @@ -20,19 +20,20 @@ public struct SessionProPlanUpdatedScreen: View { } var desription: ThemedAttributedString { switch flow { - case .update(_, let expiredOn, _, _): - "proAllSetDescription" + case .update: + guard let expiredOn else { fallthrough } + return "proAllSetDescription" .put(key: "app_pro", value: Constants.app_pro) .put(key: "pro", value: Constants.pro) .put(key: "date", value: expiredOn.formatted("MMM dd, yyyy")) .localizedFormatted(Fonts.Body.baseRegular) case .renew: - "proPlanRenewSupport" + return "proPlanRenewSupport" .put(key: "app_pro", value: Constants.app_pro) .put(key: "network_name", value: Constants.network_name) .localizedFormatted(Fonts.Body.baseRegular) case .purchase: - "proUpgraded" + return "proUpgraded" .put(key: "app_pro", value: Constants.app_pro) .put(key: "network_name", value: Constants.network_name) .localizedFormatted(Fonts.Body.baseRegular) @@ -100,5 +101,6 @@ public struct SessionProPlanUpdatedScreen: View { .padding(.horizontal, Values.mediumSpacing) .padding(.vertical, (blurSize - 111) / 2) } + .navigationBarBackButtonHidden(true) } } diff --git a/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift b/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift index 999af7e4d0..6f549e7454 100644 --- a/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift +++ b/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift @@ -115,6 +115,63 @@ public struct ListItemCell: View { // MARK: - ListItemLogoWithPro public struct ListItemLogoWithPro: View { + public enum GlowingBackgroundStyle { + case base + case large + case largeNoPaddings + + var blurSize: CGSize { + switch self { + case .base: + return CGSize( + width: UIScreen.main.bounds.width - 2 * Values.mediumSpacing - 20 * 2, + height: 111 + ) + case .large, .largeNoPaddings: + return CGSize( + width: UIScreen.main.bounds.width - 2 * Values.mediumSpacing, + height: UIScreen.main.bounds.width - 2 * Values.mediumSpacing + ) + } + } + + var shadowRadius: CGFloat { + switch self { + case .base: + return 15 + case .large, .largeNoPaddings: + return 20 + } + } + + var blurRadius: CGFloat { + switch self { + case .base: + return 20 + case .large, .largeNoPaddings: + return 30 + } + } + + var verticalPaddings: CGFloat { + switch self { + case .base, .large: + return (blurSize.height - 111) / 2 + case .largeNoPaddings: + return 0 + } + } + + var blurMaxHeight: CGFloat { + switch self { + case .large: + return UIScreen.main.bounds.width - 2 * Values.mediumSpacing + case .base, .largeNoPaddings: + return 111 + } + } + } + public enum ThemeStyle { case normal case disabled @@ -141,12 +198,19 @@ public struct ListItemLogoWithPro: View { } public struct Info: Equatable, Hashable, Differentiable { - public let style: ThemeStyle + public let themeStyle: ThemeStyle + public let glowingBackgroundStyle: GlowingBackgroundStyle public let state: State public let description: ThemedAttributedString? - public init(style: ThemeStyle, state: State, description: ThemedAttributedString? = nil) { - self.style = style + public init( + themeStyle: ThemeStyle, + glowingBackgroundStyle: GlowingBackgroundStyle, + state: State, + description: ThemedAttributedString? = nil + ) { + self.themeStyle = themeStyle + self.glowingBackgroundStyle = glowingBackgroundStyle self.state = state self.description = description } @@ -155,76 +219,76 @@ public struct ListItemLogoWithPro: View { let info: Info public var body: some View { - VStack(spacing: 0) { + ZStack(alignment: .top) { ZStack { Ellipse() - .fill(themeColor: info.style.growingBackgroundColor) + .fill(themeColor: info.themeStyle.growingBackgroundColor) .frame( - width: UIScreen.main.bounds.width - 2 * Values.mediumSpacing - 20 * 2, - height: 111 + width: info.glowingBackgroundStyle.blurSize.width, + height: info.glowingBackgroundStyle.blurSize.height ) - .shadow(radius: 15) - .opacity(0.15) - .blur(radius: 20) - - Image("SessionGreen64") - .resizable() - .renderingMode(.template) - .foregroundColor(themeColor: info.style.themeColor) - .scaledToFit() - .frame(width: 100, height: 111) + .shadow(radius: info.glowingBackgroundStyle.shadowRadius) + .opacity(0.17) + .blur(radius: info.glowingBackgroundStyle.blurRadius) } - .framing( - maxWidth: .infinity, - height: 133, - alignment: .center - ) + .frame(maxHeight: info.glowingBackgroundStyle.blurMaxHeight) - HStack(spacing: Values.smallSpacing) { - Image("SessionHeading") + VStack(spacing: 0) { + Image("SessionGreen64") .resizable() .renderingMode(.template) - .foregroundColor(themeColor: .textPrimary) + .foregroundColor(themeColor: info.themeStyle.themeColor) .scaledToFit() - .frame(width: 131, height: 18) + .frame(width: 100, height: 111) - SessionProBadge_SwiftUI(size: .medium, themeBackgroundColor: info.style.themeColor) - } - - if case .error(let message) = info.state { - HStack(spacing: Values.verySmallSpacing) { - Text(message) - Image(systemName: "exclamationmark.triangle") + HStack(spacing: Values.smallSpacing) { + Image("SessionHeading") + .resizable() + .renderingMode(.template) + .foregroundColor(themeColor: .textPrimary) + .scaledToFit() + .frame(width: 131, height: 18) + + SessionProBadge_SwiftUI(size: .medium, themeBackgroundColor: info.themeStyle.themeColor) } - .font(.Body.baseRegular) - .foregroundColor(themeColor: .warning) .padding(.top, Values.mediumSpacing) - } - - if case .loading(let message) = info.state { - HStack(spacing: Values.verySmallSpacing) { - Text(message) - ProgressView() - .tint(themeColor: .textPrimary) - .controlSize(.regular) - .scaleEffect(0.8) - .frame(width: 16, height: 16) + + if case .error(let message) = info.state { + HStack(spacing: Values.verySmallSpacing) { + Text(message) + Image(systemName: "exclamationmark.triangle") + } + .font(.Body.baseRegular) + .foregroundColor(themeColor: .warning) + .padding(.top, Values.mediumSpacing) } - .font(.Body.baseRegular) - .foregroundColor(themeColor: .textPrimary) - .padding(.top, Values.mediumSpacing) - } - - if let description = info.description { - AttributedText(description) + + if case .loading(let message) = info.state { + HStack(spacing: Values.verySmallSpacing) { + Text(message) + ProgressView() + .tint(themeColor: .textPrimary) + .controlSize(.regular) + .scaleEffect(0.8) + .frame(width: 16, height: 16) + } .font(.Body.baseRegular) .foregroundColor(themeColor: .textPrimary) - .multilineTextAlignment(.center) .padding(.top, Values.mediumSpacing) - .padding(.bottom, Values.largeSpacing) + } + + if let description = info.description { + AttributedText(description) + .font(.Body.baseRegular) + .foregroundColor(themeColor: .textPrimary) + .multilineTextAlignment(.center) + .padding(.top, Values.mediumSpacing) + .padding(.bottom, Values.largeSpacing) + } } + .padding(.vertical, info.glowingBackgroundStyle.verticalPaddings) } - .frame(maxWidth: .infinity, alignment: .center) + .frame(maxWidth: .infinity, alignment: .top) .contentShape(Rectangle()) } } From c804e63776563b112eab3f848168885a43d1c86a Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 6 Nov 2025 10:45:33 +1100 Subject: [PATCH 10/60] fix: Bottom sheet UI --- .../Components/SwiftUI/BottomSheet.swift | 2 +- .../SessionListScreen+ListItemView.swift | 35 +++++++------------ 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/SessionUIKit/Components/SwiftUI/BottomSheet.swift b/SessionUIKit/Components/SwiftUI/BottomSheet.swift index 6c25019c84..fb88effc40 100644 --- a/SessionUIKit/Components/SwiftUI/BottomSheet.swift +++ b/SessionUIKit/Components/SwiftUI/BottomSheet.swift @@ -50,7 +50,7 @@ public struct BottomSheet: View where Content: View { .onAppear { let screenHeight = UIScreen.main.bounds.height let bottomSafeInset = host.controller?.view.safeAreaInsets.bottom ?? 0 - topPadding = screenHeight - bottomSafeInset - geo.size.height - 44 - Values.largeSpacing + topPadding = screenHeight - bottomSafeInset - geo.size.height - 44 - Values.veryLargeSpacing } } diff --git a/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift b/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift index 6f549e7454..45fe8e16ef 100644 --- a/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift +++ b/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift @@ -155,21 +155,14 @@ public struct ListItemLogoWithPro: View { var verticalPaddings: CGFloat { switch self { - case .base, .large: + case .base: + return 10 + case .large: return (blurSize.height - 111) / 2 case .largeNoPaddings: return 0 } } - - var blurMaxHeight: CGFloat { - switch self { - case .large: - return UIScreen.main.bounds.width - 2 * Values.mediumSpacing - case .base, .largeNoPaddings: - return 111 - } - } } public enum ThemeStyle { @@ -220,18 +213,16 @@ public struct ListItemLogoWithPro: View { public var body: some View { ZStack(alignment: .top) { - ZStack { - Ellipse() - .fill(themeColor: info.themeStyle.growingBackgroundColor) - .frame( - width: info.glowingBackgroundStyle.blurSize.width, - height: info.glowingBackgroundStyle.blurSize.height - ) - .shadow(radius: info.glowingBackgroundStyle.shadowRadius) - .opacity(0.17) - .blur(radius: info.glowingBackgroundStyle.blurRadius) - } - .frame(maxHeight: info.glowingBackgroundStyle.blurMaxHeight) + Ellipse() + .fill(themeColor: info.themeStyle.growingBackgroundColor) + .frame( + width: info.glowingBackgroundStyle.blurSize.width, + height: info.glowingBackgroundStyle.blurSize.height + ) + .shadow(radius: info.glowingBackgroundStyle.shadowRadius) + .opacity(0.17) + .blur(radius: info.glowingBackgroundStyle.blurRadius) + .padding(.top, info.glowingBackgroundStyle.blurRadius / 2) VStack(spacing: 0) { Image("SessionGreen64") From 4400327736ad88caf0815d0c9dfb89790d22e289 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 6 Nov 2025 16:47:45 +1100 Subject: [PATCH 11/60] UI fix on bottom sheet --- .../DeveloperSettingsProViewModel.swift | 1 - SessionUIKit/Components/SwiftUI/BottomSheet.swift | 9 +++++---- .../Screens/Shared/SessionListScreen+ListItemView.swift | 4 +--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift b/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift index 7b1d61e9a0..713b9dd55e 100644 --- a/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift +++ b/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift @@ -414,7 +414,6 @@ class DeveloperSettingsProViewModel: SessionTableViewModel, NavigatableStateHold ) ) ) - dependencies[singleton: .sessionProState].isSessionProSubject.send(!state.mockCurrentUserSessionPro) } ), SessionCell.Info( diff --git a/SessionUIKit/Components/SwiftUI/BottomSheet.swift b/SessionUIKit/Components/SwiftUI/BottomSheet.swift index fb88effc40..9d61ca9339 100644 --- a/SessionUIKit/Components/SwiftUI/BottomSheet.swift +++ b/SessionUIKit/Components/SwiftUI/BottomSheet.swift @@ -44,6 +44,7 @@ public struct BottomSheet: View where Content: View { GeometryReader { geo in NavigationView { content() + .navigationTitle("") .padding(.top, 44) } .navigationViewStyle(.stack) @@ -58,12 +59,12 @@ public struct BottomSheet: View where Content: View { Button { close() } label: { - AttributedText(Lucide.Icon.x.attributedString(size: 20)) - .font(.system(size: 20)) + AttributedText(Lucide.Icon.x.attributedString(size: 28)) + .font(.system(size: 28, weight: .bold)) .foregroundColor(themeColor: .textPrimary) } - .frame(width: 24, height: 24) - .padding(Values.mediumSmallSpacing) + .frame(width: 28, height: 28) + .padding(Values.smallSpacing) } } .backgroundColor(themeColor: .backgroundPrimary) diff --git a/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift b/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift index 45fe8e16ef..9619b903e4 100644 --- a/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift +++ b/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift @@ -155,9 +155,7 @@ public struct ListItemLogoWithPro: View { var verticalPaddings: CGFloat { switch self { - case .base: - return 10 - case .large: + case .base, .large: return (blurSize.height - 111) / 2 case .largeNoPaddings: return 0 From 407be95170409b2d9ff72318d03343852dedb94b Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 6 Nov 2025 16:57:49 +1100 Subject: [PATCH 12/60] fix: cancel pro function --- .../Utilities/SessionPro/SessionProState.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift index 0dc3620c35..1d95d9229c 100644 --- a/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift +++ b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift @@ -88,7 +88,14 @@ public class SessionProState: SessionProManagerType, ProfilePictureAnimationMana guard case .active(let currentPlan, let expiredOn, _, let originatingPlatform) = self.sessionProStateSubject.value else { return } - self.sessionProStateSubject.send(.none) + self.sessionProStateSubject.send( + SessionProPlanState.active( + currentPlan: currentPlan, + expiredOn: expiredOn, + isAutoRenewing: false, + originatingPlatform: originatingPlatform + ) + ) self.shouldAnimateImageSubject.send(false) completion?(true) } From 18ee8434d00fcd45e2b8be9a85676e167e537e34 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Fri, 7 Nov 2025 09:33:33 +1100 Subject: [PATCH 13/60] fix: cancel pro function --- .../DeveloperSettingsProViewModel.swift | 3 ++- .../SessionProPaymentScreen.swift | 13 ++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift b/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift index 713b9dd55e..c01917bbfe 100644 --- a/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift +++ b/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift @@ -607,7 +607,8 @@ class DeveloperSettingsProViewModel: SessionTableViewModel, NavigatableStateHold dependencies.set(feature: .mockCurrentUserSessionProState, to: state) switch state { case .none: - dependencies[singleton: .sessionProState].cancelPro(completion: nil) + dependencies[singleton: .sessionProState].sessionProStateSubject.send(.none) + dependencies[singleton: .sessionProState].shouldAnimateImageSubject.send(false) case .active: dependencies[singleton: .sessionProState].upgradeToPro( plan: SessionProPlan(variant: .threeMonths), diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift index 0fc066121e..afcc9d7a34 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift @@ -250,7 +250,15 @@ public struct SessionProPaymentScreen: View { private func onPaymentSuccess(expiredOn: Date?) { guard !self.viewModel.isFromBottomSheet else { - isNavigationActive = true + let sessionProBottomSheet: BottomSheetHostingViewController = BottomSheetHostingViewController( + bottomSheet: BottomSheet(hasCloseButton: true) { + SessionProPlanUpdatedScreen( + flow: self.viewModel.dataModel.flow, + expiredOn: expiredOn + ) + } + ) + self.host.controller?.present(sessionProBottomSheet, animated: true) return } @@ -263,6 +271,9 @@ public struct SessionProPaymentScreen: View { viewController.modalTransitionStyle = .crossDissolve viewController.modalPresentationStyle = .overFullScreen self.host.controller?.present(viewController, animated: true) + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) { + self.host.controller?.navigationController?.popViewController(animated: false) + } } private func openTosPrivacy() { From 7a52e769c5f4723a38662ffe103aad2019189031 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Fri, 7 Nov 2025 17:16:55 +1100 Subject: [PATCH 14/60] feat: bottom sheet with dynamic height --- .../Components/SwiftUI/BottomSheet.swift | 60 ++++++++++++++----- .../SessionProPaymentScreen.swift | 12 ++-- .../SessionProPlanUpdatedScreen.swift | 4 +- 3 files changed, 56 insertions(+), 20 deletions(-) diff --git a/SessionUIKit/Components/SwiftUI/BottomSheet.swift b/SessionUIKit/Components/SwiftUI/BottomSheet.swift index 9d61ca9339..126fe6a5b1 100644 --- a/SessionUIKit/Components/SwiftUI/BottomSheet.swift +++ b/SessionUIKit/Components/SwiftUI/BottomSheet.swift @@ -4,6 +4,15 @@ import SwiftUI import Lucide import Combine +private struct SizePreferenceKey: PreferenceKey { + static var defaultValue: CGSize = .zero + static func reduce(value: inout CGSize, nextValue: () -> CGSize) { + let next = nextValue() + // Use the last non-zero size reported + if next != .zero { value = next } + } +} + public struct BottomSheet: View where Content: View { @EnvironmentObject var host: HostWrapper @State private var disposables: Set = Set() @@ -17,6 +26,7 @@ public struct BottomSheet: View where Content: View { @State private var show: Bool = true @State private var topPadding: CGFloat = 80 + @State private var contentSize: CGSize = .zero public init( hasCloseButton: Bool, @@ -41,19 +51,19 @@ public struct BottomSheet: View where Content: View { // Bottom Sheet ZStack(alignment: .topTrailing) { - GeometryReader { geo in - NavigationView { - content() - .navigationTitle("") - .padding(.top, 44) - } - .navigationViewStyle(.stack) - .onAppear { - let screenHeight = UIScreen.main.bounds.height - let bottomSafeInset = host.controller?.view.safeAreaInsets.bottom ?? 0 - topPadding = screenHeight - bottomSafeInset - geo.size.height - 44 - Values.veryLargeSpacing - } + NavigationView { + // Important: no top-level GeometryReader here that would expand. + content() + .navigationTitle("") + .padding(.top, 44) + .background( + GeometryReader { proxy in + Color.clear + .preference(key: SizePreferenceKey.self, value: proxy.size) + } + ) } + .navigationViewStyle(.stack) if hasCloseButton { Button { @@ -74,9 +84,14 @@ public struct BottomSheet: View where Content: View { alignment: .topTrailing ) } + .onPreferenceChange(SizePreferenceKey.self) { size in + contentSize = size + recomputeTopPadding() + } + .onAppear { + recomputeTopPadding() + } .padding(.top, topPadding) - .transition(.move(edge: .bottom).combined(with: .opacity)) - .animation(.spring(), value: show) } .ignoresSafeArea(edges: .bottom) .frame( @@ -99,6 +114,22 @@ public struct BottomSheet: View where Content: View { private func close() { host.controller?.presentingViewController?.dismiss(animated: true) } + + // MARK: - Layout helpers + + private func recomputeTopPadding() { + let screenHeight = UIScreen.main.bounds.height + let bottomSafeInset = host.controller?.view.safeAreaInsets.bottom ?? 0 + + let handleHeight: CGFloat = 3 + let handleSpacing: CGFloat = Values.verySmallSpacing + let headerHeight: CGFloat = handleHeight + handleSpacing + 44 + + let totalSheetHeight = headerHeight + contentSize.height + Values.veryLargeSpacing + + let computed = screenHeight - bottomSafeInset - totalSheetHeight + topPadding = max(bottomSafeInset, computed) + } } // MARK: - BottomSheetHostingViewController @@ -127,3 +158,4 @@ open class BottomSheetHostingViewController: UIHostingController Date: Mon, 10 Nov 2025 11:20:00 +1100 Subject: [PATCH 15/60] fix: Session List Screen paddings and scrollable state --- Session.xcodeproj/project.pbxproj | 5 +++- .../SessionPro/SessionProState.swift | 2 +- .../Components/SwiftUI/ScrollableList.swift | 27 +++++++++++++++++++ .../SessionListScreen+ListItemView.swift | 1 - .../Screens/Shared/SessionListScreen.swift | 12 ++++++--- 5 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 SessionUIKit/Components/SwiftUI/ScrollableList.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 0c6cf64fed..570fe48a67 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -145,6 +145,7 @@ 7BFA8AE32831D0D4001876F3 /* ContextMenuVC+EmojiReactsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFA8AE22831D0D4001876F3 /* ContextMenuVC+EmojiReactsView.swift */; }; 7BFD1A8A2745C4F000FB91B9 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD1A892745C4F000FB91B9 /* Permissions.swift */; }; 7BFD1A972747689000FB91B9 /* Session-Turn-Server in Resources */ = {isa = PBXBuildFile; fileRef = 7BFD1A962747689000FB91B9 /* Session-Turn-Server */; }; + 9405666E2EC1511400158556 /* ScrollableList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9405666D2EC1510D00158556 /* ScrollableList.swift */; }; 9409433E2C7EB81800D9D2E0 /* WebRTCSession+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9409433D2C7EB81800D9D2E0 /* WebRTCSession+Constants.swift */; }; 940943402C7ED62300D9D2E0 /* StartupError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9409433F2C7ED62300D9D2E0 /* StartupError.swift */; }; 940978652E656CED00925B36 /* SessionListScreen+ListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 940978642E656CE100925B36 /* SessionListScreen+ListItemView.swift */; }; @@ -1571,6 +1572,7 @@ 7BFA8AE22831D0D4001876F3 /* ContextMenuVC+EmojiReactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContextMenuVC+EmojiReactsView.swift"; sourceTree = ""; }; 7BFD1A892745C4F000FB91B9 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = ""; }; 7BFD1A962747689000FB91B9 /* Session-Turn-Server */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "Session-Turn-Server"; sourceTree = ""; }; + 9405666D2EC1510D00158556 /* ScrollableList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableList.swift; sourceTree = ""; }; 9409433D2C7EB81800D9D2E0 /* WebRTCSession+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebRTCSession+Constants.swift"; sourceTree = ""; }; 9409433F2C7ED62300D9D2E0 /* StartupError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupError.swift; sourceTree = ""; }; 940978642E656CE100925B36 /* SessionListScreen+ListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionListScreen+ListItemView.swift"; sourceTree = ""; }; @@ -2934,6 +2936,7 @@ 942256932C23F8DD00C0FDBF /* SwiftUI */ = { isa = PBXGroup; children = ( + 9405666D2EC1510D00158556 /* ScrollableList.swift */, 9438D5192E6951AD008C7FFE /* AnimatedToggle.swift */, 94D716812E8FA19D008294EE /* AttributedLabel.swift */, 942BA9402E4487EE007C4595 /* LightBox.swift */, @@ -6456,7 +6459,6 @@ FD8A5B1E2DBF4BBC004C689B /* ScreenLock+Errors.swift in Sources */, 942BA9412E4487F7007C4595 /* LightBox.swift in Sources */, 942BA9C12E4EA5CB007C4595 /* SessionLabelWithProBadge.swift in Sources */, - FD2272E62C351378004D8A6C /* SUIKImageFormat.swift in Sources */, 7BF8D1FB2A70AF57005F1D6E /* SwiftUI+Theme.swift in Sources */, 94363E662E60186A0004EE43 /* SessionListScreen+Section.swift in Sources */, FDB11A612DD5BDCC00BEF49F /* ImageDataManager.swift in Sources */, @@ -6484,6 +6486,7 @@ 947D7FE92D51837200E8E413 /* Text+CopyButton.swift in Sources */, FD71162A28DA83DF00B47552 /* GradientView.swift in Sources */, 94363E682E6024A40004EE43 /* SessionListScreen+AccessoryViews.swift in Sources */, + 9405666E2EC1511400158556 /* ScrollableList.swift in Sources */, 94AAB14F2E1F6CC100A6FA18 /* SessionProBadge+SwiftUI.swift in Sources */, 94AAB14B2E1E198200A6FA18 /* Modal+SwiftUI.swift in Sources */, 94AAB1532E1F8AE200A6FA18 /* ShineButton.swift in Sources */, diff --git a/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift index 1d95d9229c..04561e1530 100644 --- a/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift +++ b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift @@ -191,7 +191,7 @@ extension SessionProState: SessionProCTAManagerType { let viewModel = SessionProBottomSheetViewModel(using: dependencies) let sessionProBottomSheet: BottomSheetHostingViewController = BottomSheetHostingViewController( bottomSheet: BottomSheet(hasCloseButton: true) { - SessionListScreen(viewModel: viewModel) + SessionListScreen(viewModel: viewModel, scrollable: false) } ) presenting?(sessionProBottomSheet) diff --git a/SessionUIKit/Components/SwiftUI/ScrollableList.swift b/SessionUIKit/Components/SwiftUI/ScrollableList.swift new file mode 100644 index 0000000000..b2154a61fc --- /dev/null +++ b/SessionUIKit/Components/SwiftUI/ScrollableList.swift @@ -0,0 +1,27 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import SwiftUI + +// FIXME: After iOS 16+, we can use .scrollDisabled(true) instead + +struct ScrollableList: View { + let scrollable: Bool + let content: () -> Content + + init(scrollable: Bool, @ViewBuilder content: @escaping () -> Content) { + self.scrollable = scrollable + self.content = content + } + + var body: some View { + if scrollable { + List { content() } + } else { + LazyVStack(alignment: .leading, spacing: 0) { + content() + .padding(.horizontal, Values.mediumSpacing) + } + } + } +} + diff --git a/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift b/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift index 9619b903e4..dc071d6d2b 100644 --- a/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift +++ b/SessionUIKit/Screens/Shared/SessionListScreen+ListItemView.swift @@ -412,7 +412,6 @@ struct ListItemButton: View { RoundedRectangle(cornerRadius: 7) .fill(themeColor: enabled ? .sessionButton_primaryFilledBackground : .disabled) ) - .padding(.vertical, Values.smallSpacing) } } diff --git a/SessionUIKit/Screens/Shared/SessionListScreen.swift b/SessionUIKit/Screens/Shared/SessionListScreen.swift index 7d071b1aa6..76b3ddcbae 100644 --- a/SessionUIKit/Screens/Shared/SessionListScreen.swift +++ b/SessionUIKit/Screens/Shared/SessionListScreen.swift @@ -19,11 +19,14 @@ public struct SessionListScreen Date: Mon, 10 Nov 2025 13:12:02 +1100 Subject: [PATCH 16/60] fix: glow background in session pro updated bottom sheet --- .../SessionProSettings/SessionProPlanUpdatedScreen.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPlanUpdatedScreen.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPlanUpdatedScreen.swift index 34b9cb5bda..ac94bdcb42 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPlanUpdatedScreen.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPlanUpdatedScreen.swift @@ -8,7 +8,8 @@ public struct SessionProPlanUpdatedScreen: View { let flow: SessionProPaymentScreenContent.SessionProPlanPaymentFlow let expiredOn: Date? let isFromBottomSheet: Bool - var blurSize: CGFloat { UIScreen.main.bounds.width - 2 * Values.mediumSpacing } + var blurSizeWidth: CGFloat { UIScreen.main.bounds.width - 2 * Values.mediumSpacing } + var blurSizeHeight: CGFloat { isFromBottomSheet ? 111 : blurSizeWidth } var dismissButtonTitle: String { switch flow { case .purchase, .renew: @@ -46,9 +47,9 @@ public struct SessionProPlanUpdatedScreen: View { ZStack(alignment: .top) { Ellipse() .fill(themeColor: .settings_glowingBackground) - .frame(width: blurSize, height: blurSize) + .frame(width: blurSizeWidth, height: blurSizeHeight) .shadow(radius: 20) - .opacity(0.17) + .opacity(0.15) .blur(radius: 30) VStack(spacing: Values.mediumSpacing) { @@ -100,7 +101,7 @@ public struct SessionProPlanUpdatedScreen: View { .padding(.vertical, Values.smallSpacing) } .padding(.horizontal, Values.mediumSpacing) - .padding(.vertical, isFromBottomSheet ? 0 : (blurSize - 111) / 2) + .padding(.vertical, (blurSizeHeight - 111) / 2) } } } From 9cf0ef899a4ba288bd2cc9fe57acda86aaef5e6b Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Mon, 10 Nov 2025 15:22:56 +1100 Subject: [PATCH 17/60] feat: no billing access screen --- Session.xcodeproj/project.pbxproj | 8 +- .../DeveloperSettingsProViewModel.swift | 9 +- .../SessionProSettingsViewModel.swift | 2 +- .../SessionProState+BottomSheet.swift | 2 +- .../SessionPro/SessionProState+Models.swift | 12 +- .../SessionProPaymentScreen+CancelPlan.swift | 44 ++--- .../SessionProPaymentScreen+Models.swift | 72 ++++---- ...sionProPaymentScreen+NoBillingAccess.swift | 154 ++++++++++++++++++ .../SessionProPaymentScreen+Renew.swift | 108 ------------ ...essionProPaymentScreen+RequestRefund.swift | 44 ++--- .../SessionProPaymentScreen+SharedViews.swift | 34 ++-- .../SessionProPaymentScreen+UpdatePlan.swift | 44 ++--- .../SessionProPaymentScreen.swift | 66 ++++---- 13 files changed, 339 insertions(+), 260 deletions(-) create mode 100644 SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+NoBillingAccess.swift delete mode 100644 SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Renew.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 570fe48a67..a9c83548c1 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -197,7 +197,7 @@ 94519A952E851BF500F02723 /* SessionProPaymentScreen+UpdatePlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94519A942E851BF300F02723 /* SessionProPaymentScreen+UpdatePlan.swift */; }; 94519A972E851F1400F02723 /* SessionProPaymentScreen+RequestRefund.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94519A962E851F1400F02723 /* SessionProPaymentScreen+RequestRefund.swift */; }; 94519A992E8A1A4200F02723 /* SessionProPaymentScreen+CancelPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94519A982E8A1A3200F02723 /* SessionProPaymentScreen+CancelPlan.swift */; }; - 945E89D22E95D54700D8D907 /* SessionProPaymentScreen+Renew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945E89D12E95D54000D8D907 /* SessionProPaymentScreen+Renew.swift */; }; + 945E89D22E95D54700D8D907 /* SessionProPaymentScreen+NoBillingAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945E89D12E95D54000D8D907 /* SessionProPaymentScreen+NoBillingAccess.swift */; }; 945E89D42E95D97000D8D907 /* SessionProPaymentScreen+SharedViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945E89D32E95D96100D8D907 /* SessionProPaymentScreen+SharedViews.swift */; }; 945E89D62E9602AB00D8D907 /* SessionProPaymentScreen+Purchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945E89D52E96028B00D8D907 /* SessionProPaymentScreen+Purchase.swift */; }; 9463794A2E7131070017A014 /* SessionProManagerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 946379492E71308B0017A014 /* SessionProManagerType.swift */; }; @@ -1623,7 +1623,7 @@ 94519A962E851F1400F02723 /* SessionProPaymentScreen+RequestRefund.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProPaymentScreen+RequestRefund.swift"; sourceTree = ""; }; 94519A982E8A1A3200F02723 /* SessionProPaymentScreen+CancelPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProPaymentScreen+CancelPlan.swift"; sourceTree = ""; }; 945D9C572D6FDBE7003C4C0C /* _020_AddJobUniqueHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _020_AddJobUniqueHash.swift; sourceTree = ""; }; - 945E89D12E95D54000D8D907 /* SessionProPaymentScreen+Renew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProPaymentScreen+Renew.swift"; sourceTree = ""; }; + 945E89D12E95D54000D8D907 /* SessionProPaymentScreen+NoBillingAccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProPaymentScreen+NoBillingAccess.swift"; sourceTree = ""; }; 945E89D32E95D96100D8D907 /* SessionProPaymentScreen+SharedViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProPaymentScreen+SharedViews.swift"; sourceTree = ""; }; 945E89D52E96028B00D8D907 /* SessionProPaymentScreen+Purchase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProPaymentScreen+Purchase.swift"; sourceTree = ""; }; 946379492E71308B0017A014 /* SessionProManagerType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProManagerType.swift; sourceTree = ""; }; @@ -2998,7 +2998,7 @@ 9438D5562E6A6862008C7FFE /* SessionProPaymentScreen.swift */, 945E89D32E95D96100D8D907 /* SessionProPaymentScreen+SharedViews.swift */, 945E89D52E96028B00D8D907 /* SessionProPaymentScreen+Purchase.swift */, - 945E89D12E95D54000D8D907 /* SessionProPaymentScreen+Renew.swift */, + 945E89D12E95D54000D8D907 /* SessionProPaymentScreen+NoBillingAccess.swift */, 94519A942E851BF300F02723 /* SessionProPaymentScreen+UpdatePlan.swift */, 94519A962E851F1400F02723 /* SessionProPaymentScreen+RequestRefund.swift */, 94519A982E8A1A3200F02723 /* SessionProPaymentScreen+CancelPlan.swift */, @@ -6407,7 +6407,7 @@ 94CD95BB2E08D9E00097754D /* SessionProBadge.swift in Sources */, 942256972C23F8DD00C0FDBF /* SessionSearchBar.swift in Sources */, FD71165928E436E800B47552 /* ConfirmationModal.swift in Sources */, - 945E89D22E95D54700D8D907 /* SessionProPaymentScreen+Renew.swift in Sources */, + 945E89D22E95D54700D8D907 /* SessionProPaymentScreen+NoBillingAccess.swift in Sources */, FD981BD32DC9770E00564172 /* MentionUtilities.swift in Sources */, 7BBBDC44286EAD2D00747E59 /* TappableLabel.swift in Sources */, FD8A5B252DC05B16004C689B /* Number+Utilities.swift in Sources */, diff --git a/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift b/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift index c01917bbfe..47e9eccca2 100644 --- a/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift +++ b/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift @@ -502,7 +502,11 @@ class DeveloperSettingsProViewModel: SessionTableViewModel, NavigatableStateHold to: !state.messageFeatureAnimatedAvatar ) } - ), + ) + ] + ) + .appending( + contentsOf: [ { switch state.mockCurrentUserSessionPro { case .none, .expired: @@ -567,7 +571,8 @@ class DeveloperSettingsProViewModel: SessionTableViewModel, NavigatableStateHold } ) ] - ).compactMap { $0 } + ) + .compactMap { $0 } ) return [general, subscriptions, features] diff --git a/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift b/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift index bfdb737f9d..27b9fb584a 100644 --- a/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift +++ b/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift @@ -950,7 +950,7 @@ extension SessionProSettingsViewModel { viewModel: SessionProPaymentScreenContent.ViewModel( dependencies: dependencies, dataModel: .init( - flow: dependencies[singleton: .sessionProState].sessionProStateSubject.value.toPaymentFlow(), + flow: dependencies[singleton: .sessionProState].sessionProStateSubject.value.toPaymentFlow(using: dependencies), plans: dependencies[singleton: .sessionProState].sessionProPlans.map { $0.info() } ), isFromBottomSheet: false diff --git a/SessionMessagingKit/Utilities/SessionPro/SessionProState+BottomSheet.swift b/SessionMessagingKit/Utilities/SessionPro/SessionProState+BottomSheet.swift index 52463affe6..0efb08a23b 100644 --- a/SessionMessagingKit/Utilities/SessionPro/SessionProState+BottomSheet.swift +++ b/SessionMessagingKit/Utilities/SessionPro/SessionProState+BottomSheet.swift @@ -311,7 +311,7 @@ public class SessionProBottomSheetViewModel: SessionProBottomSheetViewModelType, viewModel: SessionProPaymentScreenContent.ViewModel( dependencies: dependencies, dataModel: .init( - flow: dependencies[singleton: .sessionProState].sessionProStateSubject.value.toPaymentFlow(), + flow: dependencies[singleton: .sessionProState].sessionProStateSubject.value.toPaymentFlow(using: dependencies), plans: dependencies[singleton: .sessionProState].sessionProPlans.map { $0.info() } ), isFromBottomSheet: true diff --git a/SessionMessagingKit/Utilities/SessionPro/SessionProState+Models.swift b/SessionMessagingKit/Utilities/SessionPro/SessionProState+Models.swift index 985eeb2e74..ba95d0eef7 100644 --- a/SessionMessagingKit/Utilities/SessionPro/SessionProState+Models.swift +++ b/SessionMessagingKit/Utilities/SessionPro/SessionProState+Models.swift @@ -5,10 +5,12 @@ import SessionUtilitiesKit import Combine public extension SessionProPlanState { - func toPaymentFlow() -> SessionProPaymentScreenContent.SessionProPlanPaymentFlow { + func toPaymentFlow(using dependencies: Dependencies) -> SessionProPaymentScreenContent.SessionProPlanPaymentFlow { switch self { case .none: - return .purchase + return .purchase( + billingAccess: !dependencies[feature: .mockInstalledFromIPA] + ) case .active(let currentPlan, let expiredOn, let isAutoRenewing, let originatingPlatform): return .update( currentPlan: currentPlan.info(), @@ -19,7 +21,8 @@ public extension SessionProPlanState { case .iOS: return .iOS case .Android: return .Android } - }() + }(), + billingAccess: !dependencies[feature: .mockInstalledFromIPA] ) case .expired(let originatingPlatform): return .renew( @@ -28,7 +31,8 @@ public extension SessionProPlanState { case .iOS: return .iOS case .Android: return .Android } - }() + }(), + billingAccess: !dependencies[feature: .mockInstalledFromIPA] ) case .refunding(let originatingPlatform, let requestedAt): return .refund( diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+CancelPlan.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+CancelPlan.swift index fd2974eb73..981d44fb08 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+CancelPlan.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+CancelPlan.swift @@ -102,29 +102,33 @@ struct CancelPlanNonOriginatingPlatformContent: View { .foregroundColor(themeColor: .textSecondary) ApproachCell( - title: "onDevice" - .put(key: "device_type", value: originatingPlatform.deviceType) - .localized(), - description: "onDeviceDescription" - .put(key: "app_name", value: Constants.app_name) - .put(key: "device_type", value: originatingPlatform.deviceType) - .put(key: "platform_account", value: originatingPlatform.account) - .put(key: "app_pro", value: Constants.app_pro) - .put(key: "pro", value: Constants.pro) - .localizedFormatted(), - variant: .device + info: .init( + title: "onDevice" + .put(key: "device_type", value: originatingPlatform.deviceType) + .localized(), + description: "onDeviceDescription" + .put(key: "app_name", value: Constants.app_name) + .put(key: "device_type", value: originatingPlatform.deviceType) + .put(key: "platform_account", value: originatingPlatform.account) + .put(key: "app_pro", value: Constants.app_pro) + .put(key: "pro", value: Constants.pro) + .localizedFormatted(), + variant: .device + ) ) ApproachCell( - title: "onPlatformWebsite" - .put(key: "platform", value: originatingPlatform.name) - .localized(), - description: "viaStoreWebsiteDescription" - .put(key: "platform_account", value: originatingPlatform.account) - .put(key: "platform_store", value: originatingPlatform.store) - .put(key: "pro", value: Constants.pro) - .localizedFormatted(Fonts.Body.baseRegular), - variant: .website + info: .init( + title: "onPlatformWebsite" + .put(key: "platform", value: originatingPlatform.name) + .localized(), + description: "viaStoreWebsiteDescription" + .put(key: "platform_account", value: originatingPlatform.account) + .put(key: "platform_store", value: originatingPlatform.store) + .put(key: "pro", value: Constants.pro) + .localizedFormatted(Fonts.Body.baseRegular), + variant: .website + ) ) } .padding(Values.mediumSpacing) diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Models.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Models.swift index 916c42b199..311031a67a 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Models.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Models.swift @@ -6,15 +6,19 @@ public enum SessionProPaymentScreenContent {} public extension SessionProPaymentScreenContent { enum SessionProPlanPaymentFlow: Equatable { - case purchase + case purchase( + billingAccess: Bool + ) case update( currentPlan: SessionProPlanInfo, expiredOn: Date, isAutoRenewing: Bool, - originatingPlatform: ClientPlatform + originatingPlatform: ClientPlatform, + billingAccess: Bool ) case renew( - originatingPlatform: ClientPlatform + originatingPlatform: ClientPlatform, + billingAccess: Bool ) case refund( originatingPlatform: ClientPlatform, @@ -26,34 +30,42 @@ public extension SessionProPaymentScreenContent { var description: ThemedAttributedString { switch self { - case .purchase: - "proChooseAccess" - .put(key: "pro", value: Constants.pro) - .localizedFormatted(Fonts.Body.baseRegular) - case .update(let currentPlan, let expiredOn, let isAutoRenewing, _): - isAutoRenewing ? - "proAccessActivatesAuto" - .put(key: "current_plan_length", value: currentPlan.durationString) - .put(key: "date", value: expiredOn.formatted("MMM dd, yyyy")) - .put(key: "pro", value: Constants.pro) - .localizedFormatted(Fonts.Body.baseRegular) : - "proAccessActivatedNotAuto" - .put(key: "date", value: expiredOn.formatted("MMM dd, yyyy")) + case .purchase(let billingAccess): + billingAccess ? + "proChooseAccess" + .put(key: "pro", value: Constants.pro) + .localizedFormatted(Fonts.Body.baseRegular) : + "proUpgradeAccess" + .put(key: "app_pro", value: Constants.app_pro) + .localizedFormatted(Fonts.Body.baseRegular) + case .update(let currentPlan, let expiredOn, let isAutoRenewing, _, _): + isAutoRenewing ? + "proAccessActivatesAuto" + .put(key: "current_plan_length", value: currentPlan.durationString) + .put(key: "date", value: expiredOn.formatted("MMM dd, yyyy")) + .put(key: "pro", value: Constants.pro) + .localizedFormatted(Fonts.Body.baseRegular) : + "proAccessActivatedNotAuto" + .put(key: "date", value: expiredOn.formatted("MMM dd, yyyy")) + .put(key: "pro", value: Constants.pro) + .localizedFormatted(Fonts.Body.baseRegular) + case .renew(_, let billingAccess): + billingAccess ? + "proChooseAccess" + .put(key: "pro", value: Constants.pro) + .localizedFormatted(Fonts.Body.baseRegular) : + "proAccessRenewStart" + .put(key: "app_pro", value: Constants.app_pro) + .put(key: "pro", value: Constants.pro) + .localizedFormatted(baseFont: Fonts.Body.baseRegular) + case .refund: + "proRefundDescription" + .localizedFormatted(baseFont: Fonts.Body.baseRegular) + case .cancel: + "proCancelSorry" .put(key: "pro", value: Constants.pro) - .localizedFormatted(Fonts.Body.baseRegular) - case .renew: - "proAccessRenewStart" - .put(key: "app_pro", value: Constants.app_pro) - .put(key: "pro", value: Constants.pro) - .localizedFormatted(baseFont: Fonts.Body.baseRegular) - case .refund: - "proRefundDescription" - .localizedFormatted(baseFont: Fonts.Body.baseRegular) - case .cancel: - "proCancelSorry" - .put(key: "pro", value: Constants.pro) - .localizedFormatted(baseFont: Fonts.Body.baseRegular) - } + .localizedFormatted(baseFont: Fonts.Body.baseRegular) + } } } diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+NoBillingAccess.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+NoBillingAccess.swift new file mode 100644 index 0000000000..ffa8f115cc --- /dev/null +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+NoBillingAccess.swift @@ -0,0 +1,154 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import SwiftUI +import Lucide + +// MARK: - No Billing Access Content + +struct NoBillingAccessContent: View { + let isRenewingPro: Bool + let originatingPlatform: SessionProPaymentScreenContent.ClientPlatform + let openPlatformStoreWebsiteAction: () -> Void + + var approaches: [ApproachCell.Info] { + isRenewingPro ? + [ + ApproachCell.Info( + title: "onLinkedDevice".localized(), + description: "proRenewDesktopLinked" + .put(key: "app_name", value: Constants.app_name) + .put(key: "app_pro", value: Constants.app_pro) + .put(key: "platform_store", value: Constants.platform_store) + .put(key: "platform_store_other", value: Constants.android_platform_store) + .put(key: "pro", value: Constants.pro) + .localizedFormatted(), + variant: .link + ), + ApproachCell.Info( + title: "proNewInstallation".localized(), + description: "proNewInstallationDescription" + .put(key: "app_name", value: Constants.app_name) + .put(key: "platform_store", value: Constants.platform_store) + .put(key: "app_pro", value: Constants.app_pro) + .put(key: "pro", value: Constants.pro) + .localizedFormatted(), + variant: .device + ), + ApproachCell.Info( + title: "onPlatformWebsite" + .put(key: "platform", value: originatingPlatform.store) + .localized(), + description: "proAccessRenewPlatformWebsite" + .put(key: "platform_account", value: originatingPlatform.account) + .put(key: "platform", value: originatingPlatform.name) + .put(key: "pro", value: Constants.pro) + .localizedFormatted(Fonts.Body.baseRegular), + variant: .website + ) + ] : + [ + ApproachCell.Info( + title: "onLinkedDevice".localized(), + description: "proUpgradeDesktopLinked" + .put(key: "app_name", value: Constants.app_name) + .put(key: "app_pro", value: Constants.app_pro) + .put(key: "platform_store", value: Constants.platform_store) + .put(key: "platform_store_other", value: Constants.android_platform_store) + .put(key: "pro", value: Constants.pro) + .localizedFormatted(), + variant: .link + ), + ApproachCell.Info( + title: "proNewInstallation".localized(), + description: "proNewInstallationDescription" + .put(key: "app_name", value: Constants.app_name) + .put(key: "platform_store", value: Constants.platform_store) + .put(key: "app_pro", value: Constants.app_pro) + .put(key: "pro", value: Constants.pro) + .localizedFormatted(), + variant: .device + ) + ] + } + + var body: some View { + VStack(spacing: Values.mediumSpacing) { + VStack( + alignment: .leading, + spacing: Values.mediumSpacing + ) { + VStack( + alignment: .leading, + spacing: Values.verySmallSpacing + ) { + Text( + isRenewingPro ? + "renewingPro" + .put(key: "pro", value: Constants.pro) + .localized() : + "proUpgradingTo" + .put(key: "pro", value: Constants.pro) + .localized() + ) + .font(.Headings.H7) + .foregroundColor(themeColor: .textPrimary) + + AttributedText( + isRenewingPro ? + "proRenewingNoAccessBilling" + .put(key: "pro", value: Constants.pro) + .put(key: "platform_store", value: Constants.platform_store) + .put(key: "platform_store_other", value: Constants.android_platform_store) + .put(key: "app_name", value: Constants.app_name) + .put(key: "build_variant", value: Constants.IPA) + .put(key: "icon", value: Lucide.Icon.squareArrowUpRight) + .localizedFormatted(Fonts.Body.baseRegular) : + "proUpgradeNoAccessBilling" + .put(key: "pro", value: Constants.pro) + .put(key: "platform_store", value: Constants.platform_store) + .put(key: "platform_store_other", value: Constants.android_platform_store) + .put(key: "app_name", value: Constants.app_name) + .put(key: "build_variant", value: Constants.IPA) + .put(key: "icon", value: Lucide.Icon.squareArrowUpRight) + .localizedFormatted(Fonts.Body.baseRegular) + ) + .font(.Body.baseRegular) + .foregroundColor(themeColor: .textPrimary) + } + + Text(isRenewingPro ? "proOptionsRenewalSubtitle".localized() : "proUpgradeOptionsTwo".localized()) + .font(.Body.baseRegular) + .foregroundColor(themeColor: .textSecondary) + + ForEach(approaches.indices, id: \.self) { index in + ApproachCell(info: approaches[index]) + } + } + .padding(Values.mediumSpacing) + .background( + RoundedRectangle(cornerRadius: 11) + .fill(themeColor: .backgroundSecondary) + ) + + if isRenewingPro { + Button { + openPlatformStoreWebsiteAction() + } label: { + Text("openPlatformStoreWebsite".put(key: "platform_store", value: originatingPlatform.store).localized()) + .font(.Body.largeRegular) + .foregroundColor(themeColor: .sessionButton_primaryFilledText) + .framing( + maxWidth: .infinity, + height: 50, + alignment: .center + ) + .background( + RoundedRectangle(cornerRadius: 7) + .fill(themeColor: .sessionButton_primaryFilledBackground) + ) + .padding(.vertical, Values.smallSpacing) + } + } + } + } +} diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Renew.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Renew.swift deleted file mode 100644 index 76b4ac91e9..0000000000 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Renew.swift +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. - -import SwiftUI -import Lucide - -// MARK: - Renew Plan No Billing Access Content - -struct RenewPlanNoBillingAccessContent: View { - let originatingPlatform: SessionProPaymentScreenContent.ClientPlatform - let openPlatformStoreWebsiteAction: () -> Void - - var body: some View { - VStack(spacing: Values.mediumSpacing) { - VStack( - alignment: .leading, - spacing: Values.mediumSpacing - ) { - VStack( - alignment: .leading, - spacing: Values.verySmallSpacing - ) { - Text( - "renewingPro" - .put(key: "pro", value: Constants.pro) - .localized() - ) - .font(.Headings.H7) - .foregroundColor(themeColor: .textPrimary) - - AttributedText( - "proRenewingNoAccessBilling" - .put(key: "pro", value: Constants.pro) - .put(key: "platform_store", value: Constants.platform_store) - .put(key: "platform_store_other", value: Constants.android_platform_store) - .put(key: "app_name", value: Constants.app_name) - .put(key: "build_variant", value: Constants.IPA) - .put(key: "icon", value: Lucide.Icon.squareArrowUpRight) - .localizedFormatted(Fonts.Body.baseRegular) - ) - .font(.Body.baseRegular) - .foregroundColor(themeColor: .textPrimary) - } - - Text("proOptionsRenewalSubtitle".localized()) - .font(.Body.baseRegular) - .foregroundColor(themeColor: .textSecondary) - - ApproachCell( - title: "onLinkedDevice".localized(), - description: "proRenewDesktopLinked" - .put(key: "app_name", value: Constants.app_name) - .put(key: "app_pro", value: Constants.app_pro) - .put(key: "platform_store", value: Constants.platform_store) - .put(key: "platform_store_other", value: Constants.android_platform_store) - .put(key: "pro", value: Constants.pro) - .localizedFormatted(), - variant: .link - ) - - ApproachCell( - title: "proNewInstallation".localized(), - description: "proNewInstallationDescription" - .put(key: "app_name", value: Constants.app_name) - .put(key: "platform_store", value: Constants.platform_store) - .put(key: "app_pro", value: Constants.app_pro) - .put(key: "pro", value: Constants.pro) - .localizedFormatted(), - variant: .device - ) - - ApproachCell( - title: "onPlatformWebsite" - .put(key: "platform", value: originatingPlatform.store) - .localized(), - description: "proAccessRenewPlatformWebsite" - .put(key: "platform_account", value: originatingPlatform.account) - .put(key: "platform", value: originatingPlatform.name) - .put(key: "pro", value: Constants.pro) - .localizedFormatted(Fonts.Body.baseRegular), - variant: .website - ) - } - .padding(Values.mediumSpacing) - .background( - RoundedRectangle(cornerRadius: 11) - .fill(themeColor: .backgroundSecondary) - ) - - Button { - openPlatformStoreWebsiteAction() - } label: { - Text("openPlatformStoreWebsite".put(key: "platform_store", value: originatingPlatform.store).localized()) - .font(.Body.largeRegular) - .foregroundColor(themeColor: .sessionButton_primaryFilledText) - .framing( - maxWidth: .infinity, - height: 50, - alignment: .center - ) - .background( - RoundedRectangle(cornerRadius: 7) - .fill(themeColor: .sessionButton_primaryFilledBackground) - ) - .padding(.vertical, Values.smallSpacing) - } - } - } -} diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+RequestRefund.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+RequestRefund.swift index a4650620e3..fe595b7b41 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+RequestRefund.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+RequestRefund.swift @@ -197,29 +197,33 @@ struct RequestRefundNonOriginatingPlatformContent: View { .foregroundColor(themeColor: .textSecondary) ApproachCell( - title: "onDevice" - .put(key: "device_type", value: originatingPlatform.deviceType) - .localized(), - description: "onDeviceDescription" - .put(key: "app_name", value: Constants.app_name) - .put(key: "device_type", value: originatingPlatform.deviceType) - .put(key: "platform_account", value: originatingPlatform.account) - .put(key: "app_pro", value: Constants.app_pro) - .put(key: "pro", value: Constants.pro) - .localizedFormatted(), - variant: .device + info: .init( + title: "onDevice" + .put(key: "device_type", value: originatingPlatform.deviceType) + .localized(), + description: "onDeviceDescription" + .put(key: "app_name", value: Constants.app_name) + .put(key: "device_type", value: originatingPlatform.deviceType) + .put(key: "platform_account", value: originatingPlatform.account) + .put(key: "app_pro", value: Constants.app_pro) + .put(key: "pro", value: Constants.pro) + .localizedFormatted(), + variant: .device + ) ) ApproachCell( - title: "viaStoreWebsite" - .put(key: "platform", value: originatingPlatform.name) - .localized(), - description: "viaStoreWebsiteDescription" - .put(key: "platform_account", value: originatingPlatform.account) - .put(key: "platform_store", value: originatingPlatform.store) - .put(key: "pro", value: Constants.pro) - .localizedFormatted(Fonts.Body.baseRegular), - variant: .website + info: .init( + title: "viaStoreWebsite" + .put(key: "platform", value: originatingPlatform.name) + .localized(), + description: "viaStoreWebsiteDescription" + .put(key: "platform_account", value: originatingPlatform.account) + .put(key: "platform_store", value: originatingPlatform.store) + .put(key: "pro", value: Constants.pro) + .localizedFormatted(Fonts.Body.baseRegular), + variant: .website + ) ) } else { VStack( diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+SharedViews.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+SharedViews.swift index d254337df9..fd99d296dd 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+SharedViews.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+SharedViews.swift @@ -156,16 +156,24 @@ struct ApproachCell: View { } } - let title: String - let description: ThemedAttributedString - let variant: Variant - let action: (() -> Void)? + struct Info { + let title: String + let description: ThemedAttributedString + let variant: Variant + let action: (() -> Void)? + + public init(title: String, description: ThemedAttributedString, variant: Variant, action: (() -> Void)? = nil) { + self.title = title + self.description = description + self.variant = variant + self.action = action + } + } + + let info: Info - init(title: String, description: ThemedAttributedString, variant: Variant, action: (() -> Void)? = nil) { - self.title = title - self.description = description - self.variant = variant - self.action = action + init(info: Info) { + self.info = info } var body: some View { @@ -177,7 +185,7 @@ struct ApproachCell: View { RoundedRectangle(cornerRadius: 4) .fill(themeColor: .value(.primary, alpha: 0.1)) - AttributedText(variant.icon.attributedString(size: 24)) + AttributedText(info.variant.icon.attributedString(size: 24)) .foregroundColor(themeColor: .primary) } .frame(width: 34, height: 34) @@ -186,11 +194,11 @@ struct ApproachCell: View { alignment: .leading, spacing: Values.verySmallSpacing ) { - Text(title) + Text(info.title) .font(.Body.baseBold) .foregroundColor(themeColor: .textPrimary) - AttributedText(description) + AttributedText(info.description) .font(.Body.baseRegular) .foregroundColor(themeColor: .textPrimary) .multilineTextAlignment(.leading) @@ -205,6 +213,6 @@ struct ApproachCell: View { RoundedRectangle(cornerRadius: 11) .stroke(themeColor: .borderSeparator) ) - .onTapGesture { action?() } + .onTapGesture { info.action?() } } } diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+UpdatePlan.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+UpdatePlan.swift index 68e2dcdfde..889704922b 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+UpdatePlan.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+UpdatePlan.swift @@ -52,29 +52,33 @@ struct UpdatePlanNonOriginatingPlatformContent: View { .foregroundColor(themeColor: .textSecondary) ApproachCell( - title: "onDevice" - .put(key: "device_type", value: originatingPlatform.deviceType) - .localized(), - description: "onDeviceDescription" - .put(key: "app_name", value: Constants.app_name) - .put(key: "device_type", value: originatingPlatform.deviceType) - .put(key: "platform_account", value: originatingPlatform.account) - .put(key: "app_pro", value: Constants.app_pro) - .put(key: "pro", value: Constants.pro) - .localizedFormatted(Fonts.Body.baseRegular), - variant: .device + info: .init( + title: "onDevice" + .put(key: "device_type", value: originatingPlatform.deviceType) + .localized(), + description: "onDeviceDescription" + .put(key: "app_name", value: Constants.app_name) + .put(key: "device_type", value: originatingPlatform.deviceType) + .put(key: "platform_account", value: originatingPlatform.account) + .put(key: "app_pro", value: Constants.app_pro) + .put(key: "pro", value: Constants.pro) + .localizedFormatted(Fonts.Body.baseRegular), + variant: .device + ) ) ApproachCell( - title: "viaStoreWebsite" - .put(key: "platform", value: originatingPlatform.name) - .localized(), - description: "viaStoreWebsiteDescription" - .put(key: "platform_account", value: originatingPlatform.account) - .put(key: "platform_store", value: originatingPlatform.store) - .put(key: "pro", value: Constants.pro) - .localizedFormatted(Fonts.Body.baseRegular), - variant: .website + info: .init( + title: "viaStoreWebsite" + .put(key: "platform", value: originatingPlatform.name) + .localized(), + description: "viaStoreWebsiteDescription" + .put(key: "platform_account", value: originatingPlatform.account) + .put(key: "platform_store", value: originatingPlatform.store) + .put(key: "pro", value: Constants.pro) + .localizedFormatted(Fonts.Body.baseRegular), + variant: .website + ) ) } .padding(Values.mediumSpacing) diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift index cd4150821e..081bc83c94 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift @@ -21,7 +21,7 @@ public struct SessionProPaymentScreen: View { public init(viewModel: SessionProPaymentScreenContent.ViewModelType) { self.viewModel = viewModel if - case .update(let currentPlan, _, _, _) = viewModel.dataModel.flow, + case .update(let currentPlan, _, _, _, _) = viewModel.dataModel.flow, let indexOfCurrentPlan = viewModel.dataModel.plans.firstIndex(of: currentPlan) { self.currentSelection = indexOfCurrentPlan @@ -47,22 +47,13 @@ public struct SessionProPaymentScreen: View { self.isShowingTooltip = false } } - - // Hidden NavigationLink for publisher-driven navigation - NavigationLink( - destination: destinationView, - isActive: $isNavigationActive - ) { - EmptyView() - } - .hidden() } } .coordinateSpace(name: coordinateSpaceName) .popoverView( content: { ZStack { - if case .update(let currentPlan, _, _, _) = viewModel.dataModel.flow, let discountPercent = currentPlan.discountPercent { + if case .update(let currentPlan, _, _, _, _) = viewModel.dataModel.flow, let discountPercent = currentPlan.discountPercent { Text( "proDiscountTooltip" .put(key: "percent", value: discountPercent) @@ -88,14 +79,6 @@ public struct SessionProPaymentScreen: View { } } - private var destinationView: some View { - SessionProPlanUpdatedScreen( - flow: self.viewModel.dataModel.flow, - expiredOn: nil, - isFromBottomSheet: false - ) - } - private var content: some View { VStack(spacing: Values.mediumSmallSpacing) { ListItemLogoWithPro( @@ -111,24 +94,27 @@ public struct SessionProPaymentScreen: View { description: viewModel.dataModel.flow.description ) ) - if case .purchase = viewModel.dataModel.flow { - SessionProPlanPurchaseContent( - currentSelection: $currentSelection, - isShowingTooltip: $isShowingTooltip, - suppressUntil: $suppressUntil, - currentPlan: nil, - sessionProPlans: viewModel.dataModel.plans, - actionButtonTitle: "upgrade".localized(), - purchaseAction: { updatePlan() }, - openTosPrivacyAction: { openTosPrivacy() } - ) - } else if case .renew(let originatingPlatform) = viewModel.dataModel.flow { - if viewModel.dataModel.plans.isEmpty { - RenewPlanNoBillingAccessContent( - originatingPlatform: originatingPlatform, - openPlatformStoreWebsiteAction: { openPlatformStoreWebsite() } + if case .purchase(let billingAccess) = viewModel.dataModel.flow { + if billingAccess { + SessionProPlanPurchaseContent( + currentSelection: $currentSelection, + isShowingTooltip: $isShowingTooltip, + suppressUntil: $suppressUntil, + currentPlan: nil, + sessionProPlans: viewModel.dataModel.plans, + actionButtonTitle: "upgrade".localized(), + purchaseAction: { updatePlan() }, + openTosPrivacyAction: { openTosPrivacy() } ) } else { + NoBillingAccessContent( + isRenewingPro: false, + originatingPlatform: .iOS, + openPlatformStoreWebsiteAction: {} + ) + } + } else if case .renew(let originatingPlatform, let billingAccess) = viewModel.dataModel.flow { + if billingAccess { SessionProPlanPurchaseContent( currentSelection: $currentSelection, isShowingTooltip: $isShowingTooltip, @@ -139,8 +125,14 @@ public struct SessionProPaymentScreen: View { purchaseAction: { updatePlan() }, openTosPrivacyAction: { openTosPrivacy() } ) + } else { + NoBillingAccessContent( + isRenewingPro: true, + originatingPlatform: originatingPlatform, + openPlatformStoreWebsiteAction: { openPlatformStoreWebsite() } + ) } - } else if case .update(let currentPlan, let expiredOn, let isAutoRenewing, let originatingPlatform) = viewModel.dataModel.flow { + } else if case .update(let currentPlan, let expiredOn, let isAutoRenewing, let originatingPlatform, _) = viewModel.dataModel.flow { if originatingPlatform == .iOS { SessionProPlanPurchaseContent( currentSelection: $currentSelection, @@ -200,7 +192,7 @@ public struct SessionProPaymentScreen: View { private func updatePlan() { let updatedPlan = viewModel.dataModel.plans[currentSelection] switch viewModel.dataModel.flow { - case .update(let currentPlan, let expiredOn, let isAutoRenewing, _): + case .update(let currentPlan, let expiredOn, let isAutoRenewing, _, _): if let updatedPlanExpiredOn = Calendar.current.date(byAdding: .month, value: updatedPlan.duration, to: expiredOn) { let confirmationModal = ConfirmationModal( info: .init( From bffd313fb90868f8fbc74607038ba6bd57bc8fff Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Mon, 10 Nov 2025 16:27:08 +1100 Subject: [PATCH 18/60] feat: confirmation modal in bottom sheet --- Session/Meta/MainAppContext.swift | 4 ++ .../SessionProState+BottomSheet.swift | 54 +++++++++++++++++++ .../Shared/SessionListScreen+Models.swift | 14 +++-- .../Screens/Shared/SessionListScreen.swift | 9 ++++ SessionUtilitiesKit/General/AppContext.swift | 3 ++ 5 files changed, 80 insertions(+), 4 deletions(-) diff --git a/Session/Meta/MainAppContext.swift b/Session/Meta/MainAppContext.swift index 2451509b67..82b225e5e1 100644 --- a/Session/Meta/MainAppContext.swift +++ b/Session/Meta/MainAppContext.swift @@ -156,4 +156,8 @@ final class MainAppContext: AppContext { } UIApplication.shared.isIdleTimerDisabled = shouldBeBlocking } + + func openUrl(_ url: URL) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } } diff --git a/SessionMessagingKit/Utilities/SessionPro/SessionProState+BottomSheet.swift b/SessionMessagingKit/Utilities/SessionPro/SessionProState+BottomSheet.swift index 0efb08a23b..1ad240b54c 100644 --- a/SessionMessagingKit/Utilities/SessionPro/SessionProState+BottomSheet.swift +++ b/SessionMessagingKit/Utilities/SessionPro/SessionProState+BottomSheet.swift @@ -322,14 +322,68 @@ public class SessionProBottomSheetViewModel: SessionProBottomSheetViewModelType, } public func showLoadingModal(title: String, description: String) { + let modal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: title, + body: .text(description, scrollMode: .never), + cancelTitle: "okay".localized(), + cancelStyle: .alert_text + ) + ) + self.transitionToScreen(modal, transitionType: .present) } public func showErrorModal(title: String, description: ThemedAttributedString) { + let modal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: title, + body: .attributedText(description, scrollMode: .never), + confirmTitle: "retry".localized(), + confirmStyle: .alert_text, + cancelTitle: "helpSupport".localized(), + cancelStyle: .alert_text, + onConfirm: { [dependencies = self.dependencies] _ in + dependencies.set( + feature: .mockCurrentUserSessionProLoadingState, + to: .loading + ) + }, + onCancel: { [weak self] _ in + self?.openUrl(Constants.session_pro_support_url) + } + ) + ) + self.transitionToScreen(modal, transitionType: .present) } public func openUrl(_ urlString: String) { + guard let url: URL = URL(string: urlString) else { return } + + let modal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "urlOpen".localized(), + body: .attributedText( + "urlOpenDescription" + .put(key: "url", value: url.absoluteString) + .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) + ), + confirmTitle: "open".localized(), + confirmStyle: .danger, + cancelTitle: "urlCopy".localized(), + cancelStyle: .alert_text, + hasCloseButton: true, + onConfirm: { [dependencies] modal in + dependencies[singleton: .appContext].openUrl(url) + modal.dismiss(animated: true) + }, + onCancel: { _ in + UIPasteboard.general.string = url.absoluteString + } + ) + ) + self.transitionToScreen(modal, transitionType: .present) } } diff --git a/SessionUIKit/Screens/Shared/SessionListScreen+Models.swift b/SessionUIKit/Screens/Shared/SessionListScreen+Models.swift index c767c162cf..d8947aa5a2 100644 --- a/SessionUIKit/Screens/Shared/SessionListScreen+Models.swift +++ b/SessionUIKit/Screens/Shared/SessionListScreen+Models.swift @@ -1,6 +1,7 @@ // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. import Foundation +import UIKit import SwiftUI import Combine @@ -32,17 +33,20 @@ public extension SessionListScreenContent { } struct NavigatableState { + let transitionToViewController: AnyPublisher<(UIViewController, TransitionType), Never> let transitionToScreen: AnyPublisher<(NavigationDestination, TransitionType), Never> let dismissScreen: AnyPublisher<(DismissType, (() -> Void)?), Never> // MARK: - Internal Variables + fileprivate let _transitionToViewController: PassthroughSubject<(UIViewController, TransitionType), Never> = PassthroughSubject() fileprivate let _transitionToScreen: PassthroughSubject<(NavigationDestination, TransitionType), Never> = PassthroughSubject() fileprivate let _dismissScreen: PassthroughSubject<(DismissType, (() -> Void)?), Never> = PassthroughSubject() // MARK: - Initialization public init() { + self.transitionToViewController = _transitionToViewController.eraseToAnyPublisher() self.transitionToScreen = _transitionToScreen.eraseToAnyPublisher() self.dismissScreen = _dismissScreen.eraseToAnyPublisher() } @@ -53,11 +57,9 @@ public extension SessionListScreenContent { viewController: UIViewController, disposables: inout Set ) { - self.transitionToScreen + self.transitionToViewController .receive(on: DispatchQueue.main) - .sink { [weak viewController] destination, transitionType in - let targetViewController = SessionHostingViewController(rootView: destination.view) - + .sink { [weak viewController] targetViewController, transitionType in switch transitionType { case .push: viewController?.navigationController?.pushViewController(targetViewController, animated: true) @@ -109,4 +111,8 @@ public extension SessionListScreenContent.NavigatableStateHolder { func transitionToScreen(_ view: V, transitionType: TransitionType = .push) { navigatableState._transitionToScreen.send((SessionListScreenContent.NavigationDestination(view), transitionType)) } + + func transitionToScreen(_ viewController: UIViewController, transitionType: TransitionType = .push) { + navigatableState._transitionToViewController.send((viewController, transitionType)) + } } diff --git a/SessionUIKit/Screens/Shared/SessionListScreen.swift b/SessionUIKit/Screens/Shared/SessionListScreen.swift index 76b3ddcbae..faa48b2a65 100644 --- a/SessionUIKit/Screens/Shared/SessionListScreen.swift +++ b/SessionUIKit/Screens/Shared/SessionListScreen.swift @@ -39,6 +39,7 @@ public struct SessionListScreen = [] private let navigatableState: SessionListScreenContent.NavigatableState? private var navigationPublisher: AnyPublisher<(SessionListScreenContent.NavigationDestination, TransitionType), Never> { navigatableState?.transitionToScreen ?? Empty().eraseToAnyPublisher() @@ -76,6 +77,14 @@ public struct SessionListScreen ()) -> UIBackgroundTaskIdentifier func endBackgroundTask(_ backgroundTaskIdentifier: UIBackgroundTaskIdentifier) + func openUrl(_ url: URL) } // MARK: - Defaults @@ -52,6 +53,7 @@ public extension AppContext { func ensureSleepBlocking(_ shouldBeBlocking: Bool, blockingObjects: [Any]) {} func beginBackgroundTask(expirationHandler: @escaping () -> ()) -> UIBackgroundTaskIdentifier { return .invalid } func endBackgroundTask(_ backgroundTaskIdentifier: UIBackgroundTaskIdentifier) {} + func openUrl(_ url: URL) {} } private final class NoopAppContext: AppContext, NoopDependency { @@ -74,4 +76,5 @@ private final class NoopAppContext: AppContext, NoopDependency { func ensureSleepBlocking(_ shouldBeBlocking: Bool, blockingObjects: [Any]) {} func beginBackgroundTask(expirationHandler: @escaping () -> ()) -> UIBackgroundTaskIdentifier { return .invalid } func endBackgroundTask(_ backgroundTaskIdentifier: UIBackgroundTaskIdentifier) {} + func openUrl(_ url: URL) {} } From 79d9194c99633f46761ae2f8f819bdac82a665db Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Mon, 10 Nov 2025 16:46:54 +1100 Subject: [PATCH 19/60] clean up --- .../ConversationVC+Interaction.swift | 24 ++++++++++++++----- .../Settings/ThreadSettingsViewModel.swift | 8 +++++-- .../MessageInfoScreen.swift | 18 +++++++++++--- Session/Settings/SettingsViewModel.swift | 9 +++---- .../SessionPro/SessionProState.swift | 3 --- .../Components/SwiftUI/ProCTAModal.swift | 3 --- .../AttachmentApprovalViewController.swift | 16 +++++++++---- 7 files changed, 56 insertions(+), 25 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index d11c78b36a..73a557908a 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -563,8 +563,12 @@ extension ConversationVC: beforePresented: { [weak self] in self?.hideInputAccessoryView() }, - onConfirm: { [weak self] in - + onConfirm: { [weak self, dependencies = viewModel.dependencies] in + dependencies[singleton: .sessionProState].showSessionProBottomSheetIfNeeded( + presenting: { bottomSheet in + self?.present(bottomSheet, animated: true) + } + ) }, afterClosed: { [weak self] in self?.showInputAccessoryView() @@ -672,8 +676,12 @@ extension ConversationVC: beforePresented: { [weak self] in self?.hideInputAccessoryView() }, - onConfirm: { [weak self] in - + onConfirm: { [weak self, dependencies = viewModel.dependencies] in + dependencies[singleton: .sessionProState].showSessionProBottomSheetIfNeeded( + presenting: { bottomSheet in + self?.present(bottomSheet, animated: true) + } + ) }, afterClosed: { [weak self] in self?.showInputAccessoryView() @@ -1720,8 +1728,12 @@ extension ConversationVC: beforePresented: { [weak self] in self?.hideInputAccessoryView() }, - onConfirm: { [weak self] in - + onConfirm: { + dependencies[singleton: .sessionProState].showSessionProBottomSheetIfNeeded( + presenting: { bottomSheet in + dependencies[singleton: .appContext].frontMostViewController?.present(bottomSheet, animated: true) + } + ) }, afterClosed: { [weak self] in self?.showInputAccessoryView() diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 6396c3c829..0dce8f5486 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -359,8 +359,12 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi dependencies[singleton: .sessionProState].showSessionProCTAIfNeeded( proCTAModalVariant, - onConfirm: { [weak self] in - + onConfirm: { + dependencies[singleton: .sessionProState].showSessionProBottomSheetIfNeeded( + presenting: { bottomSheet in + self?.transitionToScreen(bottomSheet, transitionType: .present) + } + ) }, presenting: { modal in self?.transitionToScreen(modal, transitionType: .present) diff --git a/Session/Media Viewing & Editing/MessageInfoScreen.swift b/Session/Media Viewing & Editing/MessageInfoScreen.swift index 99fef32315..6f228804d1 100644 --- a/Session/Media Viewing & Editing/MessageInfoScreen.swift +++ b/Session/Media Viewing & Editing/MessageInfoScreen.swift @@ -309,7 +309,11 @@ struct MessageInfoScreen: View { dependencies[singleton: .sessionProState].showSessionProCTAIfNeeded( proCTAVariant, onConfirm: { - + dependencies[singleton: .sessionProState].showSessionProBottomSheetIfNeeded( + presenting: { bottomSheet in + self.host.controller?.present(bottomSheet, animated: true) + } + ) }, presenting: { modal in self.host.controller?.present(modal, animated: true) @@ -406,7 +410,11 @@ struct MessageInfoScreen: View { dependencies[singleton: .sessionProState].showSessionProCTAIfNeeded( proCTAVariant, onConfirm: { - + dependencies[singleton: .sessionProState].showSessionProBottomSheetIfNeeded( + presenting: { bottomSheet in + self.host.controller?.present(bottomSheet, animated: true) + } + ) }, presenting: { modal in self.host.controller?.present(modal, animated: true) @@ -641,7 +649,11 @@ struct MessageInfoScreen: View { dependencies[singleton: .sessionProState].showSessionProCTAIfNeeded( proCTAVariant, onConfirm: { - + dependencies[singleton: .sessionProState].showSessionProBottomSheetIfNeeded( + presenting: { bottomSheet in + self.host.controller?.present(bottomSheet, animated: true) + } + ) }, presenting: { modal in self.host.controller?.present(modal, animated: true) diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index fb1560d4ab..05eecd0706 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -802,9 +802,6 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl ), onConfirm: { dependencies[singleton: .sessionProState].showSessionProBottomSheetIfNeeded( - showLoadingModal: nil, - showErrorModal: nil, - openUrl: nil, presenting: { bottomSheet in self?.transitionToScreen(bottomSheet, transitionType: .present) } @@ -866,7 +863,11 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl isSessionProActivated: dependencies[cache: .libSession].isSessionPro ), onConfirm: { - + dependencies[singleton: .sessionProState].showSessionProBottomSheetIfNeeded( + presenting: { bottomSheet in + self?.transitionToScreen(bottomSheet, transitionType: .present) + } + ) }, presenting: { modal in self?.transitionToScreen(modal, transitionType: .present) diff --git a/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift index 04561e1530..3c3361fa05 100644 --- a/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift +++ b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift @@ -183,9 +183,6 @@ extension SessionProState: SessionProCTAManagerType { } @MainActor public func showSessionProBottomSheetIfNeeded( - showLoadingModal: ((String, String) -> Void)?, - showErrorModal: ((String, ThemedAttributedString) -> Void)?, - openUrl: ((URL) -> Void)?, presenting: ((UIViewController) -> Void)? ) { let viewModel = SessionProBottomSheetViewModel(using: dependencies) diff --git a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift index 8fdae681b2..c977b4542e 100644 --- a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift +++ b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift @@ -480,9 +480,6 @@ public protocol SessionProCTAManagerType: AnyObject { ) -> Bool @MainActor func showSessionProBottomSheetIfNeeded( - showLoadingModal: ((String, String) -> Void)?, - showErrorModal: ((String, ThemedAttributedString) -> Void)?, - openUrl: ((URL) -> Void)?, presenting: ((UIViewController) -> Void)? ) } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift index e1eec2b0a0..542e7881d9 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift @@ -656,8 +656,12 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC beforePresented: { [weak self] in self?.hideInputAccessoryView() }, - onConfirm: { [weak self] in - + onConfirm: { [weak self, dependencies] in + dependencies[singleton: .sessionProState].showSessionProBottomSheetIfNeeded( + presenting: { bottomSheet in + self?.present(bottomSheet, animated: true) + } + ) }, afterClosed: { [weak self] in self?.showInputAccessoryView() @@ -700,8 +704,12 @@ extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate { beforePresented: { [weak self] in self?.hideInputAccessoryView() }, - onConfirm: { [weak self] in - + onConfirm: { [weak self, dependencies] in + dependencies[singleton: .sessionProState].showSessionProBottomSheetIfNeeded( + presenting: { bottomSheet in + self?.present(bottomSheet, animated: true) + } + ) }, afterClosed: { [weak self] in self?.showInputAccessoryView() From 3d1eaf24f7d36af58ed77d7dd9fadc55c26fc4de Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Tue, 11 Nov 2025 09:41:36 +1100 Subject: [PATCH 20/60] wip: input view action for pro bottom sheet --- SessionMessagingKit/Utilities/SessionPro/SessionProState.swift | 2 ++ SessionUIKit/Components/SwiftUI/BottomSheet.swift | 3 +++ SessionUIKit/Components/SwiftUI/ProCTAModal.swift | 2 ++ 3 files changed, 7 insertions(+) diff --git a/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift index 3c3361fa05..0533b22e40 100644 --- a/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift +++ b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift @@ -183,6 +183,8 @@ extension SessionProState: SessionProCTAManagerType { } @MainActor public func showSessionProBottomSheetIfNeeded( + beforePresented: (() -> Void)?, + afterClosed: (() -> Void)?, presenting: ((UIViewController) -> Void)? ) { let viewModel = SessionProBottomSheetViewModel(using: dependencies) diff --git a/SessionUIKit/Components/SwiftUI/BottomSheet.swift b/SessionUIKit/Components/SwiftUI/BottomSheet.swift index 126fe6a5b1..0fdaee1af0 100644 --- a/SessionUIKit/Components/SwiftUI/BottomSheet.swift +++ b/SessionUIKit/Components/SwiftUI/BottomSheet.swift @@ -18,6 +18,7 @@ public struct BottomSheet: View where Content: View { @State private var disposables: Set = Set() let hasCloseButton: Bool + let afterClosed: (() -> Void)? let content: () -> Content let cornerRadius: CGFloat = 11 @@ -30,9 +31,11 @@ public struct BottomSheet: View where Content: View { public init( hasCloseButton: Bool, + afterClosed: (() -> Void)? = nil, content: @escaping () -> Content) { self.hasCloseButton = hasCloseButton + self.afterClosed = afterClosed self.content = content } diff --git a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift index c977b4542e..73d6175301 100644 --- a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift +++ b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift @@ -480,6 +480,8 @@ public protocol SessionProCTAManagerType: AnyObject { ) -> Bool @MainActor func showSessionProBottomSheetIfNeeded( + beforePresented: (() -> Void)?, + afterClosed: (() -> Void)?, presenting: ((UIViewController) -> Void)? ) } From 539f1323f718cc89a516569dc13127e45d27a80e Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 13 Nov 2025 10:21:41 +1100 Subject: [PATCH 21/60] fix: modal and bottom sheet animation --- .../ConversationVC+Interaction.swift | 28 ++++- .../SessionPro/SessionProState.swift | 10 +- .../Components/SwiftUI/BottomSheet.swift | 102 ++++++++++-------- .../Components/SwiftUI/Modal+SwiftUI.swift | 49 +++++---- .../Components/SwiftUI/ProCTAModal.swift | 54 +++++++++- .../AttachmentApprovalViewController.swift | 20 +++- 6 files changed, 188 insertions(+), 75 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 73a557908a..f0cf0d463a 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -565,12 +565,19 @@ extension ConversationVC: }, onConfirm: { [weak self, dependencies = viewModel.dependencies] in dependencies[singleton: .sessionProState].showSessionProBottomSheetIfNeeded( + beforePresented: { [weak self] in + self?.hideInputAccessoryView() + }, + afterClosed: { [weak self] in + self?.showInputAccessoryView() + self?.snInputView.updateNumberOfCharactersLeft(self?.snInputView.text ?? "") + }, presenting: { bottomSheet in self?.present(bottomSheet, animated: true) } ) }, - afterClosed: { [weak self] in + onCancel: { [weak self] in self?.showInputAccessoryView() self?.snInputView.updateNumberOfCharactersLeft(self?.snInputView.text ?? "") }, @@ -678,12 +685,19 @@ extension ConversationVC: }, onConfirm: { [weak self, dependencies = viewModel.dependencies] in dependencies[singleton: .sessionProState].showSessionProBottomSheetIfNeeded( + beforePresented: { [weak self] in + self?.hideInputAccessoryView() + }, + afterClosed: { [weak self] in + self?.showInputAccessoryView() + self?.snInputView.updateNumberOfCharactersLeft(self?.snInputView.text ?? "") + }, presenting: { bottomSheet in self?.present(bottomSheet, animated: true) } ) }, - afterClosed: { [weak self] in + onCancel: { [weak self] in self?.showInputAccessoryView() self?.snInputView.updateNumberOfCharactersLeft(self?.snInputView.text ?? "") }, @@ -1730,15 +1744,23 @@ extension ConversationVC: }, onConfirm: { dependencies[singleton: .sessionProState].showSessionProBottomSheetIfNeeded( + beforePresented: { [weak self] in + self?.hideInputAccessoryView() + }, + afterClosed: { [weak self] in + self?.showInputAccessoryView() + self?.snInputView.updateNumberOfCharactersLeft(self?.snInputView.text ?? "") + }, presenting: { bottomSheet in dependencies[singleton: .appContext].frontMostViewController?.present(bottomSheet, animated: true) } ) }, - afterClosed: { [weak self] in + onCancel: { [weak self] in self?.showInputAccessoryView() self?.snInputView.updateNumberOfCharactersLeft(self?.snInputView.text ?? "") }, + afterClosed: nil, presenting: { modal in dependencies[singleton: .appContext].frontMostViewController?.present(modal, animated: true) } diff --git a/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift index 0533b22e40..69b7e09947 100644 --- a/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift +++ b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift @@ -152,6 +152,7 @@ extension SessionProState: SessionProCTAManagerType { dismissType: Modal.DismissType, beforePresented: (() -> Void)?, onConfirm: (() -> Void)?, + onCancel: (() -> Void)?, afterClosed: (() -> Void)?, presenting: ((UIViewController) -> Void)? ) -> Bool { @@ -174,7 +175,8 @@ extension SessionProState: SessionProCTAManagerType { dataManager: dependencies[singleton: .imageDataManager], dismissType: dismissType, afterClosed: afterClosed, - onConfirm: onConfirm + onConfirm: onConfirm, + onCancel: onCancel ) ) presenting?(sessionProModal) @@ -189,10 +191,14 @@ extension SessionProState: SessionProCTAManagerType { ) { let viewModel = SessionProBottomSheetViewModel(using: dependencies) let sessionProBottomSheet: BottomSheetHostingViewController = BottomSheetHostingViewController( - bottomSheet: BottomSheet(hasCloseButton: true) { + bottomSheet: BottomSheet( + hasCloseButton: true, + afterClosed: afterClosed, + ) { SessionListScreen(viewModel: viewModel, scrollable: false) } ) + beforePresented?() presenting?(sessionProBottomSheet) } } diff --git a/SessionUIKit/Components/SwiftUI/BottomSheet.swift b/SessionUIKit/Components/SwiftUI/BottomSheet.swift index 0fdaee1af0..a157c5e59c 100644 --- a/SessionUIKit/Components/SwiftUI/BottomSheet.swift +++ b/SessionUIKit/Components/SwiftUI/BottomSheet.swift @@ -25,7 +25,7 @@ public struct BottomSheet: View where Content: View { let shadowRadius: CGFloat = 10 let shadowOpacity: Double = 0.4 - @State private var show: Bool = true + @State private var show: Bool = false @State private var topPadding: CGFloat = 80 @State private var contentSize: CGSize = .zero @@ -47,56 +47,64 @@ public struct BottomSheet: View where Content: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .ignoresSafeArea() - VStack(spacing: Values.verySmallSpacing) { - Capsule() - .fill(themeColor: .value(.textPrimary, alpha: 0.8)) - .frame(width: 35, height: 3) - - // Bottom Sheet - ZStack(alignment: .topTrailing) { - NavigationView { - // Important: no top-level GeometryReader here that would expand. - content() - .navigationTitle("") - .padding(.top, 44) - .background( - GeometryReader { proxy in - Color.clear - .preference(key: SizePreferenceKey.self, value: proxy.size) - } - ) - } - .navigationViewStyle(.stack) + if show { + VStack(spacing: Values.verySmallSpacing) { + Capsule() + .fill(themeColor: .value(.textPrimary, alpha: 0.8)) + .frame(width: 35, height: 3) - if hasCloseButton { - Button { - close() - } label: { - AttributedText(Lucide.Icon.x.attributedString(size: 28)) - .font(.system(size: 28, weight: .bold)) - .foregroundColor(themeColor: .textPrimary) + // Bottom Sheet + ZStack(alignment: .topTrailing) { + NavigationView { + // Important: no top-level GeometryReader here that would expand. + content() + .navigationTitle("") + .padding(.top, 44) + .background( + GeometryReader { proxy in + Color.clear + .preference(key: SizePreferenceKey.self, value: proxy.size) + } + ) + } + .navigationViewStyle(.stack) + + if hasCloseButton { + Button { + close() + } label: { + AttributedText(Lucide.Icon.x.attributedString(size: 28)) + .font(.system(size: 28, weight: .bold)) + .foregroundColor(themeColor: .textPrimary) + } + .frame(width: 28, height: 28) + .padding(Values.smallSpacing) } - .frame(width: 28, height: 28) - .padding(Values.smallSpacing) } + .backgroundColor(themeColor: .backgroundPrimary) + .cornerRadius(cornerRadius, corners: [.topLeft, .topRight]) + .frame( + maxWidth: .infinity, + alignment: .topTrailing + ) } - .backgroundColor(themeColor: .backgroundPrimary) - .cornerRadius(cornerRadius, corners: [.topLeft, .topRight]) - .frame( - maxWidth: .infinity, - alignment: .topTrailing - ) - } - .onPreferenceChange(SizePreferenceKey.self) { size in - contentSize = size - recomputeTopPadding() - } - .onAppear { - recomputeTopPadding() + .transition(.move(edge: .bottom).combined(with: .opacity)) + .onPreferenceChange(SizePreferenceKey.self) { size in + contentSize = size + recomputeTopPadding() + } + .onAppear { + recomputeTopPadding() + } + .padding(.top, topPadding) } - .padding(.top, topPadding) } .ignoresSafeArea(edges: .bottom) + .onAppear { + withAnimation { + show.toggle() + } + } .frame( maxWidth: .infinity, maxHeight: .infinity, @@ -115,7 +123,11 @@ public struct BottomSheet: View where Content: View { // MARK: - Dismiss Logic private func close() { + withAnimation { + show.toggle() + } host.controller?.presentingViewController?.dismiss(animated: true) + afterClosed?() } // MARK: - Layout helpers @@ -143,7 +155,7 @@ open class BottomSheetHostingViewController: UIHostingController> super.init(rootView: modified) container.controller = self - self.modalTransitionStyle = .coverVertical + self.modalTransitionStyle = .crossDissolve self.modalPresentationStyle = .overFullScreen } diff --git a/SessionUIKit/Components/SwiftUI/Modal+SwiftUI.swift b/SessionUIKit/Components/SwiftUI/Modal+SwiftUI.swift index 2fa2c8ee75..5123621925 100644 --- a/SessionUIKit/Components/SwiftUI/Modal+SwiftUI.swift +++ b/SessionUIKit/Components/SwiftUI/Modal+SwiftUI.swift @@ -12,7 +12,7 @@ public struct Modal_SwiftUI: View where Content: View { let shadowRadius: CGFloat = 10 let shadowOpacity: Double = 0.4 - @State private var show: Bool = true + @State private var show: Bool = false public var body: some View { ZStack { @@ -23,33 +23,38 @@ public struct Modal_SwiftUI: View where Content: View { .ignoresSafeArea() .onTapGesture { close() } - // Modal - VStack { - Spacer() - - VStack(spacing: 0) { - content { internalAfterClosed in - close(internalAfterClosed) + if show { + // Modal + VStack { + Spacer() + + VStack(spacing: 0) { + content { internalAfterClosed in + close(internalAfterClosed) + } } + .backgroundColor(themeColor: .alert_background) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) + .shadow(color: Color.black.opacity(shadowOpacity), radius: shadowRadius) + .frame( + maxWidth: UIDevice.current.isIPad ? Values.iPadModalWidth : .infinity + ) + .padding(.horizontal, UIDevice.current.isIPad ? 0 : Values.veryLargeSpacing) + + Spacer() } - .backgroundColor(themeColor: .alert_background) - .clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) - .shadow(color: Color.black.opacity(shadowOpacity), radius: shadowRadius) - .frame( - maxWidth: UIDevice.current.isIPad ? Values.iPadModalWidth : .infinity - ) - .padding(.horizontal, UIDevice.current.isIPad ? 0 : Values.veryLargeSpacing) - - Spacer() + .transition(.move(edge: .bottom).combined(with: .opacity)) } - .transition(.move(edge: .bottom).combined(with: .opacity)) - .animation(.spring(), value: show) - } .frame( maxWidth: .infinity, maxHeight: .infinity ) + .onAppear { + withAnimation { + show.toggle() + } + } .gesture( DragGesture(minimumDistance: 20, coordinateSpace: .global) .onEnded { value in @@ -75,6 +80,10 @@ public struct Modal_SwiftUI: View where Content: View { } } + withAnimation { + show.toggle() + } + targetViewController?.presentingViewController?.dismiss( animated: true, completion: { diff --git a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift index 20cc999517..da2de2c44e 100644 --- a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift +++ b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift @@ -14,20 +14,22 @@ public struct ProCTAModal: View { let dismissType: Modal.DismissType let afterClosed: (() -> Void)? let onConfirm: (() -> Void)? + let onCancel: (() -> Void)? public init( variant: ProCTAModal.Variant, dataManager: ImageDataManagerType, dismissType: Modal.DismissType = .recursive, afterClosed: (() -> Void)? = nil, - onConfirm: (() -> Void)? = nil - + onConfirm: (() -> Void)? = nil, + onCancel: (() -> Void)? = nil ) { self.variant = variant self.dataManager = dataManager self.dismissType = dismissType self.afterClosed = afterClosed self.onConfirm = onConfirm + self.onCancel = onCancel } public var body: some View { @@ -249,6 +251,7 @@ public struct ProCTAModal: View { // Cancel Button Button { close(nil) + onCancel?() } label: { Text(variant.cancelButtonTitle) .font(.Body.baseRegular) @@ -479,6 +482,7 @@ public protocol SessionProCTAManagerType: AnyObject { dismissType: Modal.DismissType, beforePresented: (() -> Void)?, onConfirm: (() -> Void)?, + onCancel: (() -> Void)?, afterClosed: (() -> Void)?, presenting: ((UIViewController) -> Void)? ) -> Bool @@ -497,6 +501,7 @@ public extension SessionProCTAManagerType { _ variant: ProCTAModal.Variant, beforePresented: (() -> Void)?, onConfirm: (() -> Void)?, + onCancel: (() -> Void)?, afterClosed: (() -> Void)?, presenting: ((UIViewController) -> Void)? ) -> Bool { @@ -505,14 +510,50 @@ public extension SessionProCTAManagerType { dismissType: .recursive, beforePresented: beforePresented, onConfirm: onConfirm, + onCancel: onCancel, afterClosed: afterClosed, presenting: presenting ) } + @discardableResult @MainActor func showSessionProCTAIfNeeded( + _ variant: ProCTAModal.Variant, + beforePresented: (() -> Void)?, + onConfirm: (() -> Void)?, + onCancel: (() -> Void)?, + presenting: ((UIViewController) -> Void)? + ) -> Bool { + showSessionProCTAIfNeeded( + variant, + dismissType: .recursive, + beforePresented: beforePresented, + onConfirm: onConfirm, + onCancel: onCancel, + afterClosed: nil, + presenting: presenting + ) + } + + @discardableResult @MainActor func showSessionProCTAIfNeeded( + _ variant: ProCTAModal.Variant, + onConfirm: (() -> Void)?, + presenting: ((UIViewController) -> Void)? + ) -> Bool { + showSessionProCTAIfNeeded( + variant, + dismissType: .recursive, + beforePresented: nil, + onConfirm: onConfirm, + onCancel: nil, + afterClosed: nil, + presenting: presenting + ) + } + @discardableResult @MainActor func showSessionProCTAIfNeeded( _ variant: ProCTAModal.Variant, onConfirm: (() -> Void)?, + onCancel: (() -> Void)?, presenting: ((UIViewController) -> Void)? ) -> Bool { showSessionProCTAIfNeeded( @@ -520,6 +561,15 @@ public extension SessionProCTAManagerType { dismissType: .recursive, beforePresented: nil, onConfirm: onConfirm, + onCancel: onCancel, + afterClosed: nil, + presenting: presenting + ) + } + + @MainActor func showSessionProBottomSheetIfNeeded(presenting: ((UIViewController) -> Void)?) { + showSessionProBottomSheetIfNeeded( + beforePresented: nil, afterClosed: nil, presenting: presenting ) diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift index 542e7881d9..43ea559b6d 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift @@ -654,16 +654,23 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC guard dependencies[singleton: .sessionProState].showSessionProCTAIfNeeded( .longerMessages, beforePresented: { [weak self] in - self?.hideInputAccessoryView() + }, onConfirm: { [weak self, dependencies] in dependencies[singleton: .sessionProState].showSessionProBottomSheetIfNeeded( + beforePresented: { [weak self] in + self?.hideInputAccessoryView() + }, + afterClosed: { + self?.showInputAccessoryView() + self?.bottomToolView.attachmentTextToolbar.updateNumberOfCharactersLeft(self?.bottomToolView.attachmentTextToolbar.text ?? "") + }, presenting: { bottomSheet in self?.present(bottomSheet, animated: true) } ) }, - afterClosed: { [weak self] in + onCancel: { [weak self] in self?.showInputAccessoryView() self?.bottomToolView.attachmentTextToolbar.updateNumberOfCharactersLeft(self?.bottomToolView.attachmentTextToolbar.text ?? "") }, @@ -706,12 +713,19 @@ extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate { }, onConfirm: { [weak self, dependencies] in dependencies[singleton: .sessionProState].showSessionProBottomSheetIfNeeded( + beforePresented: { [weak self] in + self?.hideInputAccessoryView() + }, + afterClosed: { + self?.showInputAccessoryView() + self?.bottomToolView.attachmentTextToolbar.updateNumberOfCharactersLeft(self?.bottomToolView.attachmentTextToolbar.text ?? "") + }, presenting: { bottomSheet in self?.present(bottomSheet, animated: true) } ) }, - afterClosed: { [weak self] in + onCancel: { [weak self] in self?.showInputAccessoryView() self?.bottomToolView.attachmentTextToolbar.updateNumberOfCharactersLeft(self?.bottomToolView.attachmentTextToolbar.text ?? "") }, From 47ca3efa3faa463d78a732ee8071b2b55024328e Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 13 Nov 2025 13:27:08 +1100 Subject: [PATCH 22/60] feat: pending state for purchasing pro --- .../SessionPro/SessionProState.swift | 25 ++++++----- .../SessionProPaymentScreen+Purchase.swift | 43 +++++++++++-------- .../SessionProPaymentScreen.swift | 33 +++++++++++--- 3 files changed, 68 insertions(+), 33 deletions(-) diff --git a/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift index 69b7e09947..2dc5fe4f74 100644 --- a/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift +++ b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift @@ -70,18 +70,21 @@ public class SessionProState: SessionProManagerType, ProfilePictureAnimationMana } public func upgradeToPro(plan: SessionProPlan, originatingPlatform: ClientPlatform, completion: ((_ result: Bool) -> Void)?) { - dependencies.set(feature: .mockCurrentUserSessionProState, to: .active) - self.sessionProStateSubject.send( - SessionProPlanState.active( - currentPlan: plan, - expiredOn: Calendar.current.date(byAdding: .month, value: plan.variant.duration, to: Date())!, - isAutoRenewing: true, - originatingPlatform: originatingPlatform + Task { + try await Task.sleep(for: .seconds(5)) + dependencies.set(feature: .mockCurrentUserSessionProState, to: .active) + self.sessionProStateSubject.send( + SessionProPlanState.active( + currentPlan: plan, + expiredOn: Calendar.current.date(byAdding: .month, value: plan.variant.duration, to: Date())!, + isAutoRenewing: true, + originatingPlatform: originatingPlatform + ) ) - ) - self.shouldAnimateImageSubject.send(true) - dependencies.setAsync(.isProBadgeEnabled, true) - completion?(true) + self.shouldAnimateImageSubject.send(true) + dependencies.setAsync(.isProBadgeEnabled, true) + completion?(true) + } } public func cancelPro(completion: ((_ result: Bool) -> Void)?) { diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Purchase.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Purchase.swift index ba8074cf91..01d22754f6 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Purchase.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Purchase.swift @@ -9,6 +9,7 @@ struct SessionProPlanPurchaseContent: View { @Binding var currentSelection: Int @Binding var isShowingTooltip: Bool @Binding var suppressUntil: Date + @Binding var isPendingPurchase: Bool let currentPlan: SessionProPaymentScreenContent.SessionProPlanInfo? let sessionProPlans: [SessionProPaymentScreenContent.SessionProPlanInfo] @@ -27,28 +28,36 @@ struct SessionProPlanPurchaseContent: View { index: index, isCurrentPlan: (sessionProPlans[index] == currentPlan) ) + .disabled(isPendingPurchase) } Button { purchaseAction() } label: { - Text(actionButtonTitle) - .font(.Body.largeRegular) - .foregroundColor(themeColor: .sessionButton_primaryFilledText) - .framing( - maxWidth: .infinity, - height: 50, - alignment: .center - ) - .background( - RoundedRectangle(cornerRadius: 7) - .fill( - themeColor: (sessionProPlans[currentSelection] == currentPlan) ? - .disabled : - .sessionButton_primaryFilledBackground - ) - ) - .padding(.vertical, Values.smallSpacing) + ZStack { + if isPendingPurchase { + ProgressView() + .tint(themeColor: .sessionButton_primaryFilledText) + } else { + Text(actionButtonTitle) + .font(.Body.largeRegular) + .foregroundColor(themeColor: .sessionButton_primaryFilledText) + } + } + .framing( + maxWidth: .infinity, + height: 50, + alignment: .center + ) + .background( + RoundedRectangle(cornerRadius: 7) + .fill( + themeColor: (sessionProPlans[currentSelection] == currentPlan) ? + .disabled : + .sessionButton_primaryFilledBackground + ) + ) + .padding(.vertical, Values.smallSpacing) } .disabled((sessionProPlans[currentSelection] == currentPlan)) diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift index 66937e9b43..be2faa3ed6 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift @@ -8,6 +8,7 @@ public struct SessionProPaymentScreen: View { @State private var isNavigationActive: Bool = false @State var currentSelection: Int @State private var isShowingTooltip: Bool = false + @State var isPendingPurchase: Bool = false /// There is an issue on `.onAnyInteraction` of the List and `.onTapGuesture` of the TooltipsIcon. The `.onAnyInteraction` will be called first when tapping the TooltipsIcon to dismiss a tooltip. /// This will result in the tooltip will show again right after it dismissed when tapping the TooltipsIcon. This `suppressUntil` is a workaround to fix this issue. @@ -102,6 +103,7 @@ public struct SessionProPaymentScreen: View { currentSelection: $currentSelection, isShowingTooltip: $isShowingTooltip, suppressUntil: $suppressUntil, + isPendingPurchase: $isPendingPurchase, currentPlan: nil, sessionProPlans: viewModel.dataModel.plans, actionButtonTitle: "upgrade".localized(), @@ -122,6 +124,7 @@ public struct SessionProPaymentScreen: View { currentSelection: $currentSelection, isShowingTooltip: $isShowingTooltip, suppressUntil: $suppressUntil, + isPendingPurchase: $isPendingPurchase, currentPlan: nil, sessionProPlans: viewModel.dataModel.plans, actionButtonTitle: "renew".localized(), @@ -142,6 +145,7 @@ public struct SessionProPaymentScreen: View { currentSelection: $currentSelection, isShowingTooltip: $isShowingTooltip, suppressUntil: $suppressUntil, + isPendingPurchase: $isPendingPurchase, currentPlan: currentPlan, sessionProPlans: viewModel.dataModel.plans, actionButtonTitle: "updateAccess".put(key: "pro", value: Constants.pro).localized(), @@ -196,6 +200,7 @@ public struct SessionProPaymentScreen: View { } private func updatePlan() { + isPendingPurchase = true let updatedPlan = viewModel.dataModel.plans[currentSelection] switch viewModel.dataModel.flow { case .update(let currentPlan, let expiredOn, let isAutoRenewing, _, _): @@ -225,9 +230,15 @@ public struct SessionProPaymentScreen: View { onConfirm: { _ in self.viewModel.purchase( planInfo: updatedPlan, - success: { onPaymentSuccess(expiredOn: updatedPlanExpiredOn) }, + success: { + Task { @MainActor in + onPaymentSuccess(expiredOn: updatedPlanExpiredOn) + } + }, failure: { - + Task { @MainActor in + onPaymentFailed() + } } ) } @@ -238,16 +249,23 @@ public struct SessionProPaymentScreen: View { case .purchase, .renew: self.viewModel.purchase( planInfo: updatedPlan, - success: { onPaymentSuccess(expiredOn: nil) }, + success: { + Task { @MainActor in + onPaymentSuccess(expiredOn: nil) + } + }, failure: { - + Task { @MainActor in + onPaymentFailed() + } } ) default: break } } - private func onPaymentSuccess(expiredOn: Date?) { + @MainActor private func onPaymentSuccess(expiredOn: Date?) { + isPendingPurchase = false guard !self.viewModel.isFromBottomSheet else { let sessionProBottomSheet: BottomSheetHostingViewController = BottomSheetHostingViewController( bottomSheet: BottomSheet(hasCloseButton: true) { @@ -278,6 +296,11 @@ public struct SessionProPaymentScreen: View { } } + @MainActor private func onPaymentFailed() { + isPendingPurchase = false + // TODO: [PRO] + } + private func openTosPrivacy() { let modal: ModalHostingViewController = ModalHostingViewController( modal: MutipleLinksModal( From 0920961bc369c97dbb58aa31742dce4977131262 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 13 Nov 2025 13:43:07 +1100 Subject: [PATCH 23/60] fix: session pro heading in RTL shouldn't be flipped --- Session/Shared/BaseVC.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Session/Shared/BaseVC.swift b/Session/Shared/BaseVC.swift index 028c735208..afe73c1fa4 100644 --- a/Session/Shared/BaseVC.swift +++ b/Session/Shared/BaseVC.swift @@ -104,9 +104,7 @@ public class BaseVC: UIViewController { }() sessionProBadge.isHidden = !isPro - let stackView: UIStackView = UIStackView( - arrangedSubviews: MainAppContext.determineDeviceRTL() ? [ sessionProBadge, headingImageView ] : [ headingImageView, sessionProBadge ] - ) + let stackView: UIStackView = UIStackView(arrangedSubviews: [ headingImageView, sessionProBadge ]) stackView.axis = .horizontal stackView.alignment = .center stackView.spacing = 0 From a55f348e29867b2ea222902e96656d726c354eb2 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 13 Nov 2025 13:49:49 +1100 Subject: [PATCH 24/60] fix: Session Pro State logic --- Session/Conversations/Input View/InputView.swift | 7 +++---- Session/Shared/BaseVC.swift | 14 ++++++-------- .../AttachmentTextToolbar.swift | 7 +++---- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/Session/Conversations/Input View/InputView.swift b/Session/Conversations/Input View/InputView.swift index 604e0c617c..ea9248e386 100644 --- a/Session/Conversations/Input View/InputView.swift +++ b/Session/Conversations/Input View/InputView.swift @@ -219,10 +219,9 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M .sink( receiveValue: { [weak self] sessionProPlanState in let isPro: Bool = { - if case .active = sessionProPlanState { - return true - } else { - return false + switch sessionProPlanState { + case .active, .refunding : return true + case .none, .expired: return false } }() self?.sessionProBadge.isHidden = isPro diff --git a/Session/Shared/BaseVC.swift b/Session/Shared/BaseVC.swift index afe73c1fa4..1e352951b4 100644 --- a/Session/Shared/BaseVC.swift +++ b/Session/Shared/BaseVC.swift @@ -96,10 +96,9 @@ public class BaseVC: UIViewController { let sessionProBadge: SessionProBadge = SessionProBadge(size: .medium) let isPro: Bool = { - if case .active = currentUserSessionProState.sessionProStateSubject.value { - return true - } else { - return false + switch currentUserSessionProState.sessionProStateSubject.value { + case .active, .refunding : return true + case .none, .expired: return false } }() sessionProBadge.isHidden = !isPro @@ -115,10 +114,9 @@ public class BaseVC: UIViewController { .sink( receiveValue: { [weak sessionProBadge] sessionProPlanState in let isPro: Bool = { - if case .active = sessionProPlanState { - return true - } else { - return false + switch sessionProPlanState { + case .active, .refunding : return true + case .none, .expired: return false } }() sessionProBadge?.isHidden = !isPro diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift index f097ed977a..a9dc5e4e85 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift @@ -113,10 +113,9 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate { .sink( receiveValue: { [weak self] sessionProPlanState in let isPro: Bool = { - if case .active = sessionProPlanState { - return true - } else { - return false + switch sessionProPlanState { + case .active, .refunding : return true + case .none, .expired: return false } }() self?.sessionProBadge.isHidden = isPro From dd598d9f0f78f5043adfc6bf8aa0973b1cfa85dc Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 13 Nov 2025 16:01:21 +1100 Subject: [PATCH 25/60] feat: payment failure modal and string upadte --- .../SessionProPaymentScreen+Purchase.swift | 21 +++++++++- .../SessionProPaymentScreen.swift | 40 +++++++++++++++---- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Purchase.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Purchase.swift index 01d22754f6..8876b0f7db 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Purchase.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Purchase.swift @@ -14,6 +14,7 @@ struct SessionProPlanPurchaseContent: View { let currentPlan: SessionProPaymentScreenContent.SessionProPlanInfo? let sessionProPlans: [SessionProPaymentScreenContent.SessionProPlanInfo] let actionButtonTitle: String + let activationType: String let purchaseAction: () -> Void let openTosPrivacyAction: () -> Void @@ -62,7 +63,8 @@ struct SessionProPlanPurchaseContent: View { .disabled((sessionProPlans[currentSelection] == currentPlan)) AttributedText( - "proTosPrivacy" + "noteTosPrivacyPolicy" + .put(key: "action_type", value: "proUpdatingAction".localized()) .put(key: "app_pro", value: Constants.app_pro) .put(key: "icon", value: Lucide.Icon.squareArrowUpRight) .localizedFormatted(Fonts.Body.smallRegular) @@ -73,6 +75,23 @@ struct SessionProPlanPurchaseContent: View { .padding(.horizontal, Values.smallSpacing) .fixedSize(horizontal: false, vertical: true) .onTapGesture { openTosPrivacyAction() } + + if currentPlan == nil { + AttributedText( + "proTosDescription" + .put(key: "action_type", value: "proUpdatingAction".localized()) + .put(key: "activation_type", value: activationType) + .put(key: "app_pro", value: Constants.app_pro) + .put(key: "entity", value: Constants.entity_stf) + .put(key: "app_name", value: Constants.app_name) + .localizedFormatted(Fonts.Body.smallRegular) + ) + .font(.Body.smallRegular) + .foregroundColor(themeColor: .textSecondary) + .multilineTextAlignment(.center) + .padding(.horizontal, Values.smallSpacing) + .fixedSize(horizontal: false, vertical: true) + } } } } diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift index be2faa3ed6..8ef22a5658 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift @@ -107,6 +107,7 @@ public struct SessionProPaymentScreen: View { currentPlan: nil, sessionProPlans: viewModel.dataModel.plans, actionButtonTitle: "upgrade".localized(), + activationType: "proUpdatingAction".localized(), purchaseAction: { updatePlan() }, openTosPrivacyAction: { openTosPrivacy() } ) @@ -128,6 +129,7 @@ public struct SessionProPaymentScreen: View { currentPlan: nil, sessionProPlans: viewModel.dataModel.plans, actionButtonTitle: "renew".localized(), + activationType: "proRenewingAction".localized(), purchaseAction: { updatePlan() }, openTosPrivacyAction: { openTosPrivacy() } ) @@ -135,7 +137,7 @@ public struct SessionProPaymentScreen: View { NoBillingAccessContent( isRenewingPro: true, originatingPlatform: originatingPlatform, - openPlatformStoreWebsiteAction: { openPlatformStoreWebsite() } + openPlatformStoreWebsiteAction: { openUrl(Constants.google_play_store_subscriptions_url) } ) } @@ -149,6 +151,7 @@ public struct SessionProPaymentScreen: View { currentPlan: currentPlan, sessionProPlans: viewModel.dataModel.plans, actionButtonTitle: "updateAccess".put(key: "pro", value: Constants.pro).localized(), + activationType: "proUpdatingAction".localized(), purchaseAction: { updatePlan() }, openTosPrivacyAction: { openTosPrivacy() } ) @@ -158,7 +161,7 @@ public struct SessionProPaymentScreen: View { currentPlanExpiredOn: expiredOn, isAutoRenewing: isAutoRenewing, originatingPlatform: originatingPlatform, - openPlatformStoreWebsiteAction: { openPlatformStoreWebsite() } + openPlatformStoreWebsiteAction: { openUrl(Constants.google_play_store_subscriptions_url) } ) } @@ -171,7 +174,7 @@ public struct SessionProPaymentScreen: View { RequestRefundNonOriginatingPlatformContent( originatingPlatform: originatingPlatform, requestedAt: requestedAt, - openPlatformStoreWebsiteAction: { openPlatformStoreWebsite() } + openPlatformStoreWebsiteAction: { openUrl(Constants.google_play_store_subscriptions_url) } ) } @@ -192,7 +195,7 @@ public struct SessionProPaymentScreen: View { } else { CancelPlanNonOriginatingPlatformContent( originatingPlatform: originatingPlatform, - openPlatformStoreWebsiteAction: { openPlatformStoreWebsite() } + openPlatformStoreWebsiteAction: { openUrl(Constants.google_play_store_subscriptions_url) } ) } } @@ -298,7 +301,30 @@ public struct SessionProPaymentScreen: View { @MainActor private func onPaymentFailed() { isPendingPurchase = false - // TODO: [PRO] + let modal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "urlOpen".localized(), + body: .attributedText( + "paymentProError" + .put(key: "action_type", value: "proUpdatingAction".localized()) + .put(key: "pro", value: Constants.pro) + .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)), + scrollMode: .automatic + ), + confirmTitle: "retry".localized(), + confirmStyle: .alert_text, + cancelTitle: "helpSupport".localized(), + cancelStyle: .alert_text, + onConfirm: { _ in + // TODO: [PRO] Retry connecting to Pro backend + }, + onCancel: { _ in + self.openUrl(Constants.session_pro_support_url) + } + ) + ) + + self.host.controller?.present(modal, animated: true) } private func openTosPrivacy() { @@ -318,8 +344,8 @@ public struct SessionProPaymentScreen: View { self.host.controller?.present(modal, animated: true) } - private func openPlatformStoreWebsite() { - guard let url: URL = URL(string: Constants.google_play_store_subscriptions_url) else { return } + private func openUrl(_ urlString: String) { + guard let url: URL = URL(string: urlString) else { return } let modal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( From d22147258c4304323529230775d85eee729e57c9 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 13 Nov 2025 16:59:00 +1100 Subject: [PATCH 26/60] fix: string color in purchase pro screen --- .../SessionProSettings/SessionProPaymentScreen+Purchase.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Purchase.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Purchase.swift index 8876b0f7db..65c86c9b6a 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Purchase.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Purchase.swift @@ -70,7 +70,7 @@ struct SessionProPlanPurchaseContent: View { .localizedFormatted(Fonts.Body.smallRegular) ) .font(.Body.smallRegular) - .foregroundColor(themeColor: .textSecondary) + .foregroundColor(themeColor: .textPrimary) .multilineTextAlignment(.center) .padding(.horizontal, Values.smallSpacing) .fixedSize(horizontal: false, vertical: true) @@ -87,7 +87,7 @@ struct SessionProPlanPurchaseContent: View { .localizedFormatted(Fonts.Body.smallRegular) ) .font(.Body.smallRegular) - .foregroundColor(themeColor: .textSecondary) + .foregroundColor(themeColor: .textPrimary) .multilineTextAlignment(.center) .padding(.horizontal, Values.smallSpacing) .fixedSize(horizontal: false, vertical: true) From 3d60897bbafc1815744c5f29603e4c4b84a18d6a Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Fri, 14 Nov 2025 10:02:18 +1100 Subject: [PATCH 27/60] clean up --- SessionMessagingKit/Utilities/SessionPro/SessionProState.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift index 2dc5fe4f74..5130d346cb 100644 --- a/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift +++ b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift @@ -196,7 +196,7 @@ extension SessionProState: SessionProCTAManagerType { let sessionProBottomSheet: BottomSheetHostingViewController = BottomSheetHostingViewController( bottomSheet: BottomSheet( hasCloseButton: true, - afterClosed: afterClosed, + afterClosed: afterClosed ) { SessionListScreen(viewModel: viewModel, scrollable: false) } From a8db9c110bbf96e115de28e0556055bfe3bc3e2d Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Fri, 14 Nov 2025 15:34:20 +1100 Subject: [PATCH 28/60] refactor pro related function to be async --- Session.xcodeproj/project.pbxproj | 4 + .../Settings/ThreadSettingsViewModel.swift | 12 +- .../DeveloperSettingsProViewModel.swift | 20 ++-- .../SessionProSettingsViewModel.swift | 82 ++++++++------ .../UIContextualAction+Utilities.swift | 12 +- .../SessionProPaymentScreenContent.swift | 8 +- .../SessionPro/SessionProState.swift | 12 +- .../Components/SwiftUI/ProCTAModal+Type.swift | 106 ++++++++++++++++++ .../Components/SwiftUI/ProCTAModal.swift | 102 ----------------- .../SessionProPaymentScreen+Models.swift | 4 +- .../SessionProPaymentScreen.swift | 72 ++++++------ .../Types/SessionProManagerType.swift | 10 +- 12 files changed, 239 insertions(+), 205 deletions(-) create mode 100644 SessionUIKit/Components/SwiftUI/ProCTAModal+Type.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index e955f46213..7956c24f0e 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -211,6 +211,7 @@ 9463794C2E71371F0017A014 /* SessionProPaymentScreen+Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9463794B2E7137120017A014 /* SessionProPaymentScreen+Models.swift */; }; 946F5A732D5DA3AC00A5ADCE /* Punycode in Frameworks */ = {isa = PBXBuildFile; productRef = 946F5A722D5DA3AC00A5ADCE /* Punycode */; }; 9473386E2BDF5F3E00B9E169 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9473386D2BDF5F3E00B9E169 /* InfoPlist.xcstrings */; }; + 9478E84B2EC6DDBB00BFDED0 /* ProCTAModal+Type.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9478E84A2EC6DDB300BFDED0 /* ProCTAModal+Type.swift */; }; 9479981C2DD44ADC008F5CD5 /* ThreadNotificationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9479981B2DD44AC5008F5CD5 /* ThreadNotificationSettingsViewModel.swift */; }; 947D7FD62D509FC900E8E413 /* SessionNetworkAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FCF2D509FC900E8E413 /* SessionNetworkAPI.swift */; }; 947D7FD72D509FC900E8E413 /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FD22D509FC900E8E413 /* HTTPClient.swift */; }; @@ -1644,6 +1645,7 @@ 9463794B2E7137120017A014 /* SessionProPaymentScreen+Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProPaymentScreen+Models.swift"; sourceTree = ""; }; 9471CAA72CACFB4E00090FB7 /* GenerateLicenses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerateLicenses.swift; sourceTree = ""; }; 9473386D2BDF5F3E00B9E169 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; + 9478E84A2EC6DDB300BFDED0 /* ProCTAModal+Type.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProCTAModal+Type.swift"; sourceTree = ""; }; 9479981B2DD44AC5008F5CD5 /* ThreadNotificationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadNotificationSettingsViewModel.swift; sourceTree = ""; }; 947AD68F2C8968FF000B2730 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 947D7FCF2D509FC900E8E413 /* SessionNetworkAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionNetworkAPI.swift; sourceTree = ""; }; @@ -2961,6 +2963,7 @@ 94AAB1502E1F752600A6FA18 /* CyclicGradientView.swift */, 94AAB14E2E1F6CB300A6FA18 /* SessionProBadge+SwiftUI.swift */, 94AAB14C2E1F39AB00A6FA18 /* ProCTAModal.swift */, + 9478E84A2EC6DDB300BFDED0 /* ProCTAModal+Type.swift */, 94B6BAFD2E39F50E00E718BB /* UserProfileModal.swift */, 9438658E2EAB37F600DB989A /* MutipleLinksModal.swift */, 94AAB14A2E1E197800A6FA18 /* Modal+SwiftUI.swift */, @@ -6495,6 +6498,7 @@ FDE754BA2C9B97B8002A2623 /* UIDevice+Utilities.swift in Sources */, C331FFB92558FA8D00070591 /* UIView+Constraints.swift in Sources */, FD0B77B029B69A65009169BA /* TopBannerController.swift in Sources */, + 9478E84B2EC6DDBB00BFDED0 /* ProCTAModal+Type.swift in Sources */, 94E9BC0D2C7BFBDA006984EA /* Localization+Style.swift in Sources */, FDA335F52D91157A007E0EB6 /* SessionImageView.swift in Sources */, 94519A972E851F1400F02723 /* SessionProPaymentScreen+RequestRefund.swift in Sources */, diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 8745a5a4bc..9d45d217a3 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -2042,11 +2042,13 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi ), dataManager: dependencies[singleton: .imageDataManager], onConfirm: { [dependencies] in - dependencies[singleton: .sessionProState].upgradeToPro( - plan: SessionProPlan(variant: .threeMonths), - originatingPlatform: .iOS, - completion: nil - ) + Task { + await dependencies[singleton: .sessionProState].upgradeToPro( + plan: SessionProPlan(variant: .threeMonths), + originatingPlatform: .iOS, + completion: nil + ) + } } ) ) diff --git a/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift b/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift index 47e9eccca2..6d38ce2a0b 100644 --- a/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift +++ b/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift @@ -615,15 +615,21 @@ class DeveloperSettingsProViewModel: SessionTableViewModel, NavigatableStateHold dependencies[singleton: .sessionProState].sessionProStateSubject.send(.none) dependencies[singleton: .sessionProState].shouldAnimateImageSubject.send(false) case .active: - dependencies[singleton: .sessionProState].upgradeToPro( - plan: SessionProPlan(variant: .threeMonths), - originatingPlatform: dependencies[feature: .proPlanOriginatingPlatform], - completion: nil - ) + Task { + await dependencies[singleton: .sessionProState].upgradeToPro( + plan: SessionProPlan(variant: .threeMonths), + originatingPlatform: dependencies[feature: .proPlanOriginatingPlatform], + completion: nil + ) + } case .expired: - dependencies[singleton: .sessionProState].expirePro(completion: nil) + Task { + await dependencies[singleton: .sessionProState].expirePro(completion: nil) + } case .refunding: - dependencies[singleton: .sessionProState].requestRefund(completion: nil) + Task { + await dependencies[singleton: .sessionProState].requestRefund(completion: nil) + } } } diff --git a/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift b/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift index a397d24900..bbdbd01968 100644 --- a/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift +++ b/Session/Settings/SessionProSettings/SessionProSettingsViewModel.swift @@ -864,7 +864,17 @@ public class SessionProSettingsViewModel: SessionListScreenContent.ViewModelType ) ) ), - onTap: { [weak viewModel] in viewModel?.recoverProPlan() } + onTap: { [weak viewModel] in + Task { + await viewModel? + .dependencies[singleton: .sessionProState] + .recoverPro { [weak viewModel] result in + DispatchQueue.main.async { + viewModel?.recoverProPlanCompletionHandler(result) + } + } + } + } ), ] case .refunding: [] @@ -875,7 +885,7 @@ public class SessionProSettingsViewModel: SessionListScreenContent.ViewModelType // MARK: - Interactions extension SessionProSettingsViewModel { - func openUrl(_ urlString: String) { + @MainActor func openUrl(_ urlString: String) { guard let url: URL = URL(string: urlString) else { return } let modal: ConfirmationModal = ConfirmationModal( @@ -904,7 +914,7 @@ extension SessionProSettingsViewModel { self.transitionToScreen(modal, transitionType: .present) } - func showLoadingModal( + @MainActor func showLoadingModal( from item: ListItem, title: String, description: String @@ -923,7 +933,7 @@ extension SessionProSettingsViewModel { self.transitionToScreen(modal, transitionType: .present) } - func showErrorModal( + @MainActor func showErrorModal( from item: ListItem, title: String, description: ThemedAttributedString @@ -969,49 +979,47 @@ extension SessionProSettingsViewModel { self.transitionToScreen(viewController) } - func recoverProPlan() { - dependencies[singleton: .sessionProState].recoverPro { [weak self] result in - let modal: ConfirmationModal = ConfirmationModal( - info: ConfirmationModal.Info( - title: ( - result ? + @MainActor func recoverProPlanCompletionHandler(_ result: Bool) { + let modal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: ( + result ? "proAccessRestored" .put(key: "pro", value: Constants.pro) .localized() : - "proAccessNotFound" - .put(key: "pro", value: Constants.pro) - .localized() - ), - body: .text( - ( - result ? + "proAccessNotFound" + .put(key: "pro", value: Constants.pro) + .localized() + ), + body: .text( + ( + result ? "proAccessRestoredDescription" .put(key: "app_name", value: Constants.app_name) .put(key: "pro", value: Constants.pro) .localized() : - "proAccessNotFoundDescription" - .put(key: "app_name", value: Constants.app_name) - .put(key: "pro", value: Constants.pro) - .localized() - ), - scrollMode: .never + "proAccessNotFoundDescription" + .put(key: "app_name", value: Constants.app_name) + .put(key: "pro", value: Constants.pro) + .localized() ), - confirmTitle: (result ? nil : "helpSupport".localized()), - cancelTitle: (result ? "okay".localized() : "close".localized()), - cancelStyle: (result ? .textPrimary : .danger), - dismissOnConfirm: false, - onConfirm: { [weak self] modal in - guard result == false else { - return modal.dismiss(animated: true) - } - - self?.openUrl(Constants.session_pro_recovery_support_url) + scrollMode: .never + ), + confirmTitle: (result ? nil : "helpSupport".localized()), + cancelTitle: (result ? "okay".localized() : "close".localized()), + cancelStyle: (result ? .textPrimary : .danger), + dismissOnConfirm: false, + onConfirm: { [weak self] modal in + guard result == false else { + return modal.dismiss(animated: true) } - ) + + self?.openUrl(Constants.session_pro_recovery_support_url) + } ) - - self?.transitionToScreen(modal, transitionType: .present) - } + ) + + self.transitionToScreen(modal, transitionType: .present) } func cancelPlan() { diff --git a/Session/Utilities/UIContextualAction+Utilities.swift b/Session/Utilities/UIContextualAction+Utilities.swift index 587b0aa847..3484f2644e 100644 --- a/Session/Utilities/UIContextualAction+Utilities.swift +++ b/Session/Utilities/UIContextualAction+Utilities.swift @@ -246,11 +246,13 @@ public extension UIContextualAction { completionHandler(true) }, onConfirm: { [dependencies] in - dependencies[singleton: .sessionProState].upgradeToPro( - plan: SessionProPlan(variant: .threeMonths), - originatingPlatform: .iOS, - completion: nil - ) + Task { + await dependencies[singleton: .sessionProState].upgradeToPro( + plan: SessionProPlan(variant: .threeMonths), + originatingPlatform: .iOS, + completion: nil + ) + } } ) ) diff --git a/SessionMessagingKit/Utilities/SessionPro/SessionProPaymentScreenContent.swift b/SessionMessagingKit/Utilities/SessionPro/SessionProPaymentScreenContent.swift index b87266e35d..2af049615a 100644 --- a/SessionMessagingKit/Utilities/SessionPro/SessionProPaymentScreenContent.swift +++ b/SessionMessagingKit/Utilities/SessionPro/SessionProPaymentScreenContent.swift @@ -20,9 +20,9 @@ extension SessionProPaymentScreenContent { self.isFromBottomSheet = isFromBottomSheet } - public func purchase(planInfo: SessionProPlanInfo, success: (() -> Void)?, failure: (() -> Void)?) { + public func purchase(planInfo: SessionProPlanInfo, success: (() -> Void)?, failure: (() -> Void)?) async { let plan: SessionProPlan = SessionProPlan.from(planInfo) - dependencies[singleton: .sessionProState].upgradeToPro( + await dependencies[singleton: .sessionProState].upgradeToPro( plan: plan, originatingPlatform: .iOS ) { result in @@ -34,8 +34,8 @@ extension SessionProPaymentScreenContent { } } - public func cancelPro(success: (() -> Void)?, failure: (() -> Void)?) { - dependencies[singleton: .sessionProState].cancelPro { result in + public func cancelPro(success: (() -> Void)?, failure: (() -> Void)?) async { + await dependencies[singleton: .sessionProState].cancelPro { result in if result { success?() } else { diff --git a/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift index 5130d346cb..4bba62d37b 100644 --- a/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift +++ b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift @@ -69,7 +69,7 @@ public class SessionProState: SessionProManagerType, ProfilePictureAnimationMana ) } - public func upgradeToPro(plan: SessionProPlan, originatingPlatform: ClientPlatform, completion: ((_ result: Bool) -> Void)?) { + public func upgradeToPro(plan: SessionProPlan, originatingPlatform: ClientPlatform, completion: ((_ result: Bool) -> Void)?) async { Task { try await Task.sleep(for: .seconds(5)) dependencies.set(feature: .mockCurrentUserSessionProState, to: .active) @@ -87,7 +87,7 @@ public class SessionProState: SessionProManagerType, ProfilePictureAnimationMana } } - public func cancelPro(completion: ((_ result: Bool) -> Void)?) { + public func cancelPro(completion: ((_ result: Bool) -> Void)?) async { guard case .active(let currentPlan, let expiredOn, _, let originatingPlatform) = self.sessionProStateSubject.value else { return } @@ -103,7 +103,7 @@ public class SessionProState: SessionProManagerType, ProfilePictureAnimationMana completion?(true) } - public func requestRefund(completion: ((_ result: Bool) -> Void)?) { + public func requestRefund(completion: ((_ result: Bool) -> Void)?) async { dependencies.set(feature: .mockCurrentUserSessionProState, to: .refunding) self.sessionProStateSubject.send( SessionProPlanState.refunding( @@ -115,7 +115,7 @@ public class SessionProState: SessionProManagerType, ProfilePictureAnimationMana completion?(true) } - public func expirePro(completion: ((_ result: Bool) -> Void)?) { + public func expirePro(completion: ((_ result: Bool) -> Void)?) async { dependencies.set(feature: .mockCurrentUserSessionProState, to: .expired) self.sessionProStateSubject.send( SessionProPlanState.expired( @@ -126,12 +126,12 @@ public class SessionProState: SessionProManagerType, ProfilePictureAnimationMana completion?(true) } - public func recoverPro(completion: ((_ result: Bool) -> Void)?) { + public func recoverPro(completion: ((_ result: Bool) -> Void)?) async { guard dependencies[feature: .proPlanToRecover] == true && dependencies[feature: .mockCurrentUserSessionProLoadingState] == .success else { completion?(false) return } - upgradeToPro( + await upgradeToPro( plan: SessionProPlan(variant: .threeMonths), originatingPlatform: dependencies[feature: .proPlanOriginatingPlatform], completion: completion diff --git a/SessionUIKit/Components/SwiftUI/ProCTAModal+Type.swift b/SessionUIKit/Components/SwiftUI/ProCTAModal+Type.swift new file mode 100644 index 0000000000..f1b010ea28 --- /dev/null +++ b/SessionUIKit/Components/SwiftUI/ProCTAModal+Type.swift @@ -0,0 +1,106 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SwiftUI + +// MARK: - SessionProCTAManagerType + +public protocol SessionProCTAManagerType: AnyObject { + @discardableResult @MainActor func showSessionProCTAIfNeeded( + _ variant: ProCTAModal.Variant, + dismissType: Modal.DismissType, + beforePresented: (() -> Void)?, + onConfirm: (() -> Void)?, + onCancel: (() -> Void)?, + afterClosed: (() -> Void)?, + presenting: ((UIViewController) -> Void)? + ) -> Bool + + @MainActor func showSessionProBottomSheetIfNeeded( + beforePresented: (() -> Void)?, + afterClosed: (() -> Void)?, + presenting: ((UIViewController) -> Void)? + ) +} + +// MARK: - Convenience + +public extension SessionProCTAManagerType { + @discardableResult @MainActor func showSessionProCTAIfNeeded( + _ variant: ProCTAModal.Variant, + beforePresented: (() -> Void)?, + onConfirm: (() -> Void)?, + onCancel: (() -> Void)?, + afterClosed: (() -> Void)?, + presenting: ((UIViewController) -> Void)? + ) -> Bool { + showSessionProCTAIfNeeded( + variant, + dismissType: .recursive, + beforePresented: beforePresented, + onConfirm: onConfirm, + onCancel: onCancel, + afterClosed: afterClosed, + presenting: presenting + ) + } + + @discardableResult @MainActor func showSessionProCTAIfNeeded( + _ variant: ProCTAModal.Variant, + beforePresented: (() -> Void)?, + onConfirm: (() -> Void)?, + onCancel: (() -> Void)?, + presenting: ((UIViewController) -> Void)? + ) -> Bool { + showSessionProCTAIfNeeded( + variant, + dismissType: .recursive, + beforePresented: beforePresented, + onConfirm: onConfirm, + onCancel: onCancel, + afterClosed: nil, + presenting: presenting + ) + } + + @discardableResult @MainActor func showSessionProCTAIfNeeded( + _ variant: ProCTAModal.Variant, + onConfirm: (() -> Void)?, + presenting: ((UIViewController) -> Void)? + ) -> Bool { + showSessionProCTAIfNeeded( + variant, + dismissType: .recursive, + beforePresented: nil, + onConfirm: onConfirm, + onCancel: nil, + afterClosed: nil, + presenting: presenting + ) + } + + @discardableResult @MainActor func showSessionProCTAIfNeeded( + _ variant: ProCTAModal.Variant, + onConfirm: (() -> Void)?, + onCancel: (() -> Void)?, + presenting: ((UIViewController) -> Void)? + ) -> Bool { + showSessionProCTAIfNeeded( + variant, + dismissType: .recursive, + beforePresented: nil, + onConfirm: onConfirm, + onCancel: onCancel, + afterClosed: nil, + presenting: presenting + ) + } + + @MainActor func showSessionProBottomSheetIfNeeded(presenting: ((UIViewController) -> Void)?) { + showSessionProBottomSheetIfNeeded( + beforePresented: nil, + afterClosed: nil, + presenting: presenting + ) + } +} diff --git a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift index da2de2c44e..d3e1c8a084 100644 --- a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift +++ b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift @@ -474,108 +474,6 @@ public extension ProCTAModal { } } -// MARK: - SessionProCTAManagerType - -public protocol SessionProCTAManagerType: AnyObject { - @discardableResult @MainActor func showSessionProCTAIfNeeded( - _ variant: ProCTAModal.Variant, - dismissType: Modal.DismissType, - beforePresented: (() -> Void)?, - onConfirm: (() -> Void)?, - onCancel: (() -> Void)?, - afterClosed: (() -> Void)?, - presenting: ((UIViewController) -> Void)? - ) -> Bool - - @MainActor func showSessionProBottomSheetIfNeeded( - beforePresented: (() -> Void)?, - afterClosed: (() -> Void)?, - presenting: ((UIViewController) -> Void)? - ) -} - -// MARK: - Convenience - -public extension SessionProCTAManagerType { - @discardableResult @MainActor func showSessionProCTAIfNeeded( - _ variant: ProCTAModal.Variant, - beforePresented: (() -> Void)?, - onConfirm: (() -> Void)?, - onCancel: (() -> Void)?, - afterClosed: (() -> Void)?, - presenting: ((UIViewController) -> Void)? - ) -> Bool { - showSessionProCTAIfNeeded( - variant, - dismissType: .recursive, - beforePresented: beforePresented, - onConfirm: onConfirm, - onCancel: onCancel, - afterClosed: afterClosed, - presenting: presenting - ) - } - - @discardableResult @MainActor func showSessionProCTAIfNeeded( - _ variant: ProCTAModal.Variant, - beforePresented: (() -> Void)?, - onConfirm: (() -> Void)?, - onCancel: (() -> Void)?, - presenting: ((UIViewController) -> Void)? - ) -> Bool { - showSessionProCTAIfNeeded( - variant, - dismissType: .recursive, - beforePresented: beforePresented, - onConfirm: onConfirm, - onCancel: onCancel, - afterClosed: nil, - presenting: presenting - ) - } - - @discardableResult @MainActor func showSessionProCTAIfNeeded( - _ variant: ProCTAModal.Variant, - onConfirm: (() -> Void)?, - presenting: ((UIViewController) -> Void)? - ) -> Bool { - showSessionProCTAIfNeeded( - variant, - dismissType: .recursive, - beforePresented: nil, - onConfirm: onConfirm, - onCancel: nil, - afterClosed: nil, - presenting: presenting - ) - } - - @discardableResult @MainActor func showSessionProCTAIfNeeded( - _ variant: ProCTAModal.Variant, - onConfirm: (() -> Void)?, - onCancel: (() -> Void)?, - presenting: ((UIViewController) -> Void)? - ) -> Bool { - showSessionProCTAIfNeeded( - variant, - dismissType: .recursive, - beforePresented: nil, - onConfirm: onConfirm, - onCancel: onCancel, - afterClosed: nil, - presenting: presenting - ) - } - - @MainActor func showSessionProBottomSheetIfNeeded(presenting: ((UIViewController) -> Void)?) { - showSessionProBottomSheetIfNeeded( - beforePresented: nil, - afterClosed: nil, - presenting: presenting - ) - } -} - // MARK: - Previews struct ProCTAModal_Previews: PreviewProvider { diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Models.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Models.swift index 311031a67a..aeff767ae7 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Models.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Models.swift @@ -158,8 +158,8 @@ public extension SessionProPaymentScreenContent { var errorString: String? { get set } var isFromBottomSheet: Bool { get } - func purchase(planInfo: SessionProPlanInfo, success: (() -> Void)?, failure: (() -> Void)?) - func cancelPro(success: (() -> Void)?, failure: (() -> Void)?) + func purchase(planInfo: SessionProPlanInfo, success: (() -> Void)?, failure: (() -> Void)?) async + func cancelPro(success: (() -> Void)?, failure: (() -> Void)?) async } } diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift index 8ef22a5658..0261ac01f9 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift @@ -182,14 +182,18 @@ public struct SessionProPaymentScreen: View { if originatingPlatform == .iOS { CancelPlanOriginatingPlatformContent( cancelPlanAction: { - viewModel.cancelPro( - success: { - host.controller?.navigationController?.popViewController(animated: true) - }, - failure: { - - } - ) + Task { + await viewModel.cancelPro( + success: { + DispatchQueue.main.async { + host.controller?.navigationController?.popViewController(animated: true) + } + }, + failure: { + // TODO: [Pro] Failed to cancel plan + } + ) + } } ) } else { @@ -231,38 +235,42 @@ public struct SessionProPaymentScreen: View { ), confirmTitle: "update".localized(), onConfirm: { _ in - self.viewModel.purchase( - planInfo: updatedPlan, - success: { - Task { @MainActor in - onPaymentSuccess(expiredOn: updatedPlanExpiredOn) - } - }, - failure: { - Task { @MainActor in - onPaymentFailed() + Task { + await viewModel.purchase( + planInfo: updatedPlan, + success: { + Task { @MainActor in + onPaymentSuccess(expiredOn: updatedPlanExpiredOn) + } + }, + failure: { + Task { @MainActor in + onPaymentFailed() + } } - } - ) + ) + } } ) ) self.host.controller?.present(confirmationModal, animated: true) } case .purchase, .renew: - self.viewModel.purchase( - planInfo: updatedPlan, - success: { - Task { @MainActor in - onPaymentSuccess(expiredOn: nil) - } - }, - failure: { - Task { @MainActor in - onPaymentFailed() + Task { + await viewModel.purchase( + planInfo: updatedPlan, + success: { + Task { @MainActor in + onPaymentSuccess(expiredOn: nil) + } + }, + failure: { + Task { @MainActor in + onPaymentFailed() + } } - } - ) + ) + } default: break } } diff --git a/SessionUtilitiesKit/Types/SessionProManagerType.swift b/SessionUtilitiesKit/Types/SessionProManagerType.swift index 986178c02e..a5f10c1b06 100644 --- a/SessionUtilitiesKit/Types/SessionProManagerType.swift +++ b/SessionUtilitiesKit/Types/SessionProManagerType.swift @@ -7,11 +7,11 @@ public protocol SessionProManagerType: AnyObject { var sessionProStateSubject: CurrentValueSubject { get } var sessionProStatePublisher: AnyPublisher { get } var sessionProPlans: [SessionProPlan] { get } - func upgradeToPro(plan: SessionProPlan, originatingPlatform: ClientPlatform, completion: ((_ result: Bool) -> Void)?) - func cancelPro(completion: ((_ result: Bool) -> Void)?) - func requestRefund(completion: ((_ result: Bool) -> Void)?) - func expirePro(completion: ((_ result: Bool) -> Void)?) - func recoverPro(completion: ((_ result: Bool) -> Void)?) + func upgradeToPro(plan: SessionProPlan, originatingPlatform: ClientPlatform, completion: ((_ result: Bool) -> Void)?) async + func cancelPro(completion: ((_ result: Bool) -> Void)?) async + func requestRefund(completion: ((_ result: Bool) -> Void)?) async + func expirePro(completion: ((_ result: Bool) -> Void)?) async + func recoverPro(completion: ((_ result: Bool) -> Void)?) async // This function is only for QA purpose func updateOriginatingPlatform(_ newValue: ClientPlatform) } From 5d0df3ee6e4be8c8904e0f215e4fc87dc4150189 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Fri, 14 Nov 2025 16:19:16 +1100 Subject: [PATCH 29/60] add TODOs --- .../Utilities/SessionPro/SessionProState.swift | 6 ++++++ .../SessionProSettings/SessionProPaymentScreen.swift | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift index 4bba62d37b..0ee5cb91de 100644 --- a/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift +++ b/SessionMessagingKit/Utilities/SessionPro/SessionProState.swift @@ -36,6 +36,7 @@ public class SessionProState: SessionProManagerType, ProfilePictureAnimationMana public init(using dependencies: Dependencies) { self.dependencies = dependencies + // TODO: [PRO] Get the pro state of current user let originatingPlatform: ClientPlatform = dependencies[feature: .proPlanOriginatingPlatform] switch dependencies[feature: .mockCurrentUserSessionProState] { case .none: @@ -70,6 +71,7 @@ public class SessionProState: SessionProManagerType, ProfilePictureAnimationMana } public func upgradeToPro(plan: SessionProPlan, originatingPlatform: ClientPlatform, completion: ((_ result: Bool) -> Void)?) async { + // TODO: [PRO] Upgrade to Pro Task { try await Task.sleep(for: .seconds(5)) dependencies.set(feature: .mockCurrentUserSessionProState, to: .active) @@ -88,6 +90,7 @@ public class SessionProState: SessionProManagerType, ProfilePictureAnimationMana } public func cancelPro(completion: ((_ result: Bool) -> Void)?) async { + // TODO: [PRO] Cancel Pro: This is more like just cancel subscription guard case .active(let currentPlan, let expiredOn, _, let originatingPlatform) = self.sessionProStateSubject.value else { return } @@ -104,6 +107,7 @@ public class SessionProState: SessionProManagerType, ProfilePictureAnimationMana } public func requestRefund(completion: ((_ result: Bool) -> Void)?) async { + // TODO: [PRO] Request refund dependencies.set(feature: .mockCurrentUserSessionProState, to: .refunding) self.sessionProStateSubject.send( SessionProPlanState.refunding( @@ -116,6 +120,7 @@ public class SessionProState: SessionProManagerType, ProfilePictureAnimationMana } public func expirePro(completion: ((_ result: Bool) -> Void)?) async { + // TODO: [PRO] Mannualy expire pro state, maybe just for QA as we have backend to determine if pro is expired dependencies.set(feature: .mockCurrentUserSessionProState, to: .expired) self.sessionProStateSubject.send( SessionProPlanState.expired( @@ -127,6 +132,7 @@ public class SessionProState: SessionProManagerType, ProfilePictureAnimationMana } public func recoverPro(completion: ((_ result: Bool) -> Void)?) async { + // TODO: [PRO] Recover from an existing pro plan guard dependencies[feature: .proPlanToRecover] == true && dependencies[feature: .mockCurrentUserSessionProLoadingState] == .success else { completion?(false) return diff --git a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift index 0261ac01f9..357f64f236 100644 --- a/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift +++ b/SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift @@ -190,7 +190,7 @@ public struct SessionProPaymentScreen: View { } }, failure: { - // TODO: [Pro] Failed to cancel plan + // TODO: [PRO] Failed to cancel plan } ) } From 5f01900eb7e26e5b14fa9245c7c1d70be121a985 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 19 Nov 2025 13:21:06 +1100 Subject: [PATCH 30/60] fix UI issues after merge --- Session/Home/HomeViewModel.swift | 46 +++++++++++-------- ...essionListScreen+ListItemLogoWithPro.swift | 26 +++++------ 2 files changed, 39 insertions(+), 33 deletions(-) diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 2fb31b152c..5d89260fbf 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -634,29 +634,35 @@ public class HomeViewModel: NavigatableStateHolder { case .active(_, let expiredOn, _ , _): let expiryInSeconds: TimeInterval = expiredOn.timeIntervalSinceNow guard expiryInSeconds <= 7 * 24 * 60 * 60 else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self, dependencies] in - dependencies[singleton: .sessionProState].showSessionProCTAIfNeeded( - .expiring(timeLeft: expiryInSeconds.formatted(format: .long, allowedUnits: [ .day, .hour, .minute ])), - onConfirm: { - - }, - presenting: { modal in - self?.transitionToScreen(modal, transitionType: .present) - } - ) - } + scheduleExpiringSessionProCTA(expiryInSeconds.formatted(format: .long, allowedUnits: [ .day, .hour, .minute ])) case .expired: - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self, dependencies] in - dependencies[singleton: .sessionProState].showSessionProCTAIfNeeded( - .expiring(timeLeft: nil), - onConfirm: { - - }, - presenting: { modal in - self?.transitionToScreen(modal, transitionType: .present) - } + scheduleExpiringSessionProCTA(nil) + } + } + + private func scheduleExpiringSessionProCTA(_ timeLeft: String?) { + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self, dependencies] in + dependencies[singleton: .sessionProState].showSessionProCTAIfNeeded( + .expiring(timeLeft: timeLeft), + onConfirm: { + let viewController: SessionHostingViewController = SessionHostingViewController( + rootView: SessionProPaymentScreen( + viewModel: SessionProPaymentScreenContent.ViewModel( + dependencies: dependencies, + dataModel: .init( + flow: dependencies[singleton: .sessionProState].sessionProStateSubject.value.toPaymentFlow(using: dependencies), + plans: dependencies[singleton: .sessionProState].sessionProPlans.map { $0.info() } + ), + isFromBottomSheet: false + ) + ) ) + self?.transitionToScreen(viewController) + }, + presenting: { modal in + self?.transitionToScreen(modal, transitionType: .present) } + ) } } diff --git a/SessionUIKit/Screens/SessionListScreen/ListItemViews/SessionListScreen+ListItemLogoWithPro.swift b/SessionUIKit/Screens/SessionListScreen/ListItemViews/SessionListScreen+ListItemLogoWithPro.swift index 22365d8bf9..79570c89e5 100644 --- a/SessionUIKit/Screens/SessionListScreen/ListItemViews/SessionListScreen+ListItemLogoWithPro.swift +++ b/SessionUIKit/Screens/SessionListScreen/ListItemViews/SessionListScreen+ListItemLogoWithPro.swift @@ -101,19 +101,19 @@ public struct ListItemLogoWithPro: View { let info: Info public var body: some View { - VStack(spacing: 0) { - ZStack { - Ellipse() - .fill(themeColor: info.themeStyle.glowingBackgroundColor) - .frame( - width: info.glowingBackgroundStyle.blurSize.width, - height: info.glowingBackgroundStyle.blurSize.height - ) - .opacity(0.17) - .shadow(radius: info.glowingBackgroundStyle.shadowRadius) - .blur(radius: info.glowingBackgroundStyle.blurRadius) - .padding(.top, info.glowingBackgroundStyle.blurRadius / 2) - + ZStack(alignment: .top) { + Ellipse() + .fill(themeColor: info.themeStyle.glowingBackgroundColor) + .frame( + width: info.glowingBackgroundStyle.blurSize.width, + height: info.glowingBackgroundStyle.blurSize.height + ) + .opacity(0.17) + .shadow(radius: info.glowingBackgroundStyle.shadowRadius) + .blur(radius: info.glowingBackgroundStyle.blurRadius) + .padding(.top, info.glowingBackgroundStyle.blurRadius / 2) + + VStack(spacing: 0) { Image("SessionGreen64") .resizable() .renderingMode(.template) From a925cab99738a118a0ce771112333ef649c654ad Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 19 Nov 2025 15:19:14 +1100 Subject: [PATCH 31/60] fix: padding for session logo with glowing background --- .../ListItemViews/SessionListScreen+ListItemLogoWithPro.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SessionUIKit/Screens/SessionListScreen/ListItemViews/SessionListScreen+ListItemLogoWithPro.swift b/SessionUIKit/Screens/SessionListScreen/ListItemViews/SessionListScreen+ListItemLogoWithPro.swift index 79570c89e5..d2b5530b03 100644 --- a/SessionUIKit/Screens/SessionListScreen/ListItemViews/SessionListScreen+ListItemLogoWithPro.swift +++ b/SessionUIKit/Screens/SessionListScreen/ListItemViews/SessionListScreen+ListItemLogoWithPro.swift @@ -47,7 +47,7 @@ public struct ListItemLogoWithPro: View { var verticalPaddings: CGFloat { switch self { case .base, .large: - return (blurSize.height - 111) / 2 + return (blurSize.height - 96) / 2 case .largeNoPaddings: return 0 } @@ -169,6 +169,7 @@ public struct ListItemLogoWithPro: View { } .padding(.vertical, info.glowingBackgroundStyle.verticalPaddings) } + .padding(.top, Values.smallSpacing) .frame(maxWidth: .infinity, alignment: .top) .contentShape(Rectangle()) } From 6329ff4abcc3620e3ef7e0be7303983194fd4460 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Mon, 24 Nov 2025 15:04:25 +1100 Subject: [PATCH 32/60] update image resource for pro CTAs --- Session.xcodeproj/project.pbxproj | 15 ++++++--------- .../Meta/WebPImages/AnimatedProfileCTA.webp | Bin 473102 -> 471126 bytes Session/Meta/WebPImages/GenericCTA.webp | Bin 536814 -> 532798 bytes .../Meta/WebPImages/HigherCharLimitCTA.webp | Bin 577484 -> 533698 bytes .../WebPImages/PinnedConversationsCTA.webp | Bin 535376 -> 543970 bytes .../Components/SwiftUI/ProCTAModal.swift | 4 ++-- 6 files changed, 8 insertions(+), 11 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 7c99fbd159..ccf3121763 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -227,6 +227,8 @@ 94805EC32EB48ED50055EBBC /* SessionProPaymentScreenContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EC22EB48EC40055EBBC /* SessionProPaymentScreenContent.swift */; }; 94805EC62EB823B80055EBBC /* DismissType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EC52EB823B00055EBBC /* DismissType.swift */; }; 94805EC82EB834D40055EBBC /* UINavigationController+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94805EC72EB834CD0055EBBC /* UINavigationController+Utilities.swift */; }; + 948615BC2ED40D38000A5666 /* GenericCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 948615BB2ED40D38000A5666 /* GenericCTA.webp */; }; + 948615BD2ED40D38000A5666 /* GenericCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 948615BB2ED40D38000A5666 /* GenericCTA.webp */; }; 9499E6032DDD9BF900091434 /* ExpandableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */; }; 9499E68B2DF92F4E00091434 /* ThreadNotificationSettingsViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */; }; 94A6B9DB2DD6BF7C00DB4B44 /* Constants+Apple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */; }; @@ -253,14 +255,11 @@ 94CD962D2E1B85920097754D /* InputViewButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD962B2E1B85920097754D /* InputViewButton.swift */; }; 94CD962E2E1B85920097754D /* InputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD962A2E1B85920097754D /* InputTextView.swift */; }; 94CD96302E1B88430097754D /* CGRect+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD962F2E1B88430097754D /* CGRect+Utilities.swift */; }; - 94CD96402E1BABE90097754D /* GenericCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94CD963C2E1BABE90097754D /* GenericCTA.webp */; }; 94CD96412E1BABE90097754D /* HigherCharLimitCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94CD963E2E1BABE90097754D /* HigherCharLimitCTA.webp */; }; - 94CD96432E1BAC0F0097754D /* GenericCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94CD963C2E1BABE90097754D /* GenericCTA.webp */; }; 94CD96452E1BAC0F0097754D /* HigherCharLimitCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94CD963E2E1BABE90097754D /* HigherCharLimitCTA.webp */; }; 94D716802E8F6363008294EE /* HighlightMentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D7167F2E8F6362008294EE /* HighlightMentionView.swift */; }; 94D716822E8FA1A0008294EE /* AttributedLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D716812E8FA19D008294EE /* AttributedLabel.swift */; }; 94D716862E933958008294EE /* SessionProBadge+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D716852E93394B008294EE /* SessionProBadge+Utilities.swift */; }; - 94D716912E9379BA008294EE /* MentionUtilities+Attributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D716902E9379A4008294EE /* MentionUtilities+Attributes.swift */; }; 94E9BC0D2C7BFBDA006984EA /* Localization+Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */; }; A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; }; A163E8AB16F3F6AA0094D68B /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A163E8AA16F3F6A90094D68B /* Security.framework */; }; @@ -1695,6 +1694,7 @@ 94805EC22EB48EC40055EBBC /* SessionProPaymentScreenContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProPaymentScreenContent.swift; sourceTree = ""; }; 94805EC52EB823B00055EBBC /* DismissType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissType.swift; sourceTree = ""; }; 94805EC72EB834CD0055EBBC /* UINavigationController+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Utilities.swift"; sourceTree = ""; }; + 948615BB2ED40D38000A5666 /* GenericCTA.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = GenericCTA.webp; sourceTree = ""; }; 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableLabel.swift; sourceTree = ""; }; 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadNotificationSettingsViewModelSpec.swift; sourceTree = ""; }; 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Constants+Apple.swift"; sourceTree = ""; }; @@ -1719,12 +1719,10 @@ 94CD962A2E1B85920097754D /* InputTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputTextView.swift; sourceTree = ""; }; 94CD962B2E1B85920097754D /* InputViewButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputViewButton.swift; sourceTree = ""; }; 94CD962F2E1B88430097754D /* CGRect+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Utilities.swift"; sourceTree = ""; }; - 94CD963C2E1BABE90097754D /* GenericCTA.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = GenericCTA.webp; sourceTree = ""; }; 94CD963E2E1BABE90097754D /* HigherCharLimitCTA.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = HigherCharLimitCTA.webp; sourceTree = ""; }; 94D7167F2E8F6362008294EE /* HighlightMentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightMentionView.swift; sourceTree = ""; }; 94D716812E8FA19D008294EE /* AttributedLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedLabel.swift; sourceTree = ""; }; 94D716852E93394B008294EE /* SessionProBadge+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProBadge+Utilities.swift"; sourceTree = ""; }; - 94D716902E9379A4008294EE /* MentionUtilities+Attributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MentionUtilities+Attributes.swift"; sourceTree = ""; }; 94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Localization+Style.swift"; sourceTree = ""; }; A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; A163E8AA16F3F6A90094D68B /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; @@ -3175,8 +3173,8 @@ 94B6BB052E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp */, 94AAB15B2E24C97400A6FA18 /* AnimatedProfileCTA.webp */, 94AAB1562E24BD3700A6FA18 /* PinnedConversationsCTA.webp */, - 94CD963C2E1BABE90097754D /* GenericCTA.webp */, 94CD963E2E1BABE90097754D /* HigherCharLimitCTA.webp */, + 948615BB2ED40D38000A5666 /* GenericCTA.webp */, ); path = WebPImages; sourceTree = ""; @@ -6127,7 +6125,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 94CD96432E1BAC0F0097754D /* GenericCTA.webp in Resources */, + 948615BD2ED40D38000A5666 /* GenericCTA.webp in Resources */, 94AAB15E2E24C97400A6FA18 /* AnimatedProfileCTA.webp in Resources */, 94CD96452E1BAC0F0097754D /* HigherCharLimitCTA.webp in Resources */, 94AAB1582E24BD3700A6FA18 /* PinnedConversationsCTA.webp in Resources */, @@ -6205,6 +6203,7 @@ B67EBF5D19194AC60084CCFD /* Settings.bundle in Resources */, 34CF0787203E6B78005C4D61 /* busy_tone_ansi.caf in Resources */, 45A2F005204473A3002E978A /* NewMessage.aifc in Resources */, + 948615BC2ED40D38000A5666 /* GenericCTA.webp in Resources */, 45B74A882044AAB600CD42F8 /* aurora.aifc in Resources */, 45B74A742044AAB600CD42F8 /* aurora-quiet.aifc in Resources */, 9420CAC82E584B5800F738F6 /* GroupAdminCTA.webp in Resources */, @@ -6241,7 +6240,6 @@ 45B74A7D2044AAB600CD42F8 /* popcorn-quiet.aifc in Resources */, 45B74A822044AAB600CD42F8 /* pulse.aifc in Resources */, C3CA3AC8255CDB2900F4C6D4 /* spanish.txt in Resources */, - 94CD96402E1BABE90097754D /* GenericCTA.webp in Resources */, 94CD96412E1BABE90097754D /* HigherCharLimitCTA.webp in Resources */, 45B74A802044AAB600CD42F8 /* pulse-quiet.aifc in Resources */, 45B74A8B2044AAB600CD42F8 /* synth.aifc in Resources */, @@ -7466,7 +7464,6 @@ FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */, FDE754B62C9B96BB002A2623 /* WebRTCSession+UI.swift in Sources */, FD71163828E2C50700B47552 /* SessionTableViewModel.swift in Sources */, - C328251F25CA3A900062D0A7 /* QuoteView.swift in Sources */, 7B3A39322980D02B002FE4AC /* SessionCarouselView.swift in Sources */, B8EB20F02640F7F000773E52 /* OpenGroupInvitationView.swift in Sources */, C328254025CA55880062D0A7 /* ContextMenuVC.swift in Sources */, diff --git a/Session/Meta/WebPImages/AnimatedProfileCTA.webp b/Session/Meta/WebPImages/AnimatedProfileCTA.webp index 9d2ee88e15b2448df19613fc8eb94e3c1c7bd0dc..d6571612a7973f0a0d3cd5c6b3af113225cc13fb 100644 GIT binary patch literal 471126 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