mirror of
https://github.com/lux-tel/session-ios.git
synced 2026-10-30 20:38:32 +00:00
# Conflicts: # Session.xcodeproj/project.pbxproj # Session/Conversations/ConversationVC+Interaction.swift # Session/Conversations/ConversationVC.swift # Session/Conversations/Settings/ThreadSettingsViewModel.swift # Session/Home/App Review/AppReviewPromptModel.swift # Session/Home/HomeViewModel.swift # Session/Media Viewing & Editing/MessageInfoScreen.swift # Session/Meta/AppDelegate.swift # Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift # Session/Settings/SettingsViewModel.swift # Session/Shared/BaseVC.swift # Session/Utilities/UIContextualAction+Utilities.swift # SessionMessagingKit/Database/Models/SessionThread.swift # SessionMessagingKit/SessionPro/SessionProPaymentScreenContent.swift # SessionMessagingKit/SessionPro/SessionProSettingsViewModel.swift # SessionMessagingKit/SessionPro/SessionProState.swift # SessionMessagingKit/Shared Models/SessionThreadViewModel.swift # SessionNotificationServiceExtension/NotificationServiceExtension.swift # SessionUIKit/Components/Input View/InputView.swift # SessionUIKit/Components/SwiftUI/ProCTAModal.swift # SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+CancelPlan.swift # SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Models.swift # SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Purchase.swift # SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+Renew.swift # SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+RequestRefund.swift # SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen+UpdatePlan.swift # SessionUIKit/Screens/Settings/SessionProSettings/SessionProPaymentScreen.swift # SessionUIKit/Screens/Settings/SessionProSettings/SessionProPlanUpdatedScreen.swift # SessionUIKit/Style Guide/Constants+Apple.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift
583 lines
26 KiB
Swift
583 lines
26 KiB
Swift
// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import SwiftUI
|
|
import Lucide
|
|
import Combine
|
|
|
|
public struct ProCTAModal: View {
|
|
public enum Variant {
|
|
case generic(renew: Bool)
|
|
case longerMessages(renew: Bool)
|
|
case animatedProfileImage(isSessionProActivated: Bool, renew: Bool)
|
|
case morePinnedConvos(isGrandfathered: Bool, renew: Bool)
|
|
case groupLimit(isAdmin: Bool, isSessionProActivated: Bool, proBadgeImage: UIImage)
|
|
case expiring(timeLeft: String?)
|
|
}
|
|
|
|
@EnvironmentObject var host: HostWrapper
|
|
@State var proCTAImageHeight: CGFloat = 0
|
|
|
|
private let variant: ProCTAModal.Variant
|
|
private let dataManager: ImageDataManagerType
|
|
private let sessionProUIManager: SessionProUIManagerType
|
|
|
|
let dismissType: Modal.DismissType
|
|
let onConfirm: (() -> Void)?
|
|
let onCancel: (() -> Void)?
|
|
let afterClosed: (() -> Void)?
|
|
|
|
public init(
|
|
variant: ProCTAModal.Variant,
|
|
dataManager: ImageDataManagerType,
|
|
sessionProUIManager: SessionProUIManagerType,
|
|
dismissType: Modal.DismissType = .recursive,
|
|
onConfirm: (() -> Void)? = nil,
|
|
onCancel: (() -> Void)? = nil,
|
|
afterClosed: (() -> Void)? = nil
|
|
) {
|
|
self.variant = variant
|
|
self.dataManager = dataManager
|
|
self.sessionProUIManager = sessionProUIManager
|
|
self.dismissType = dismissType
|
|
self.onConfirm = onConfirm
|
|
self.onCancel = onCancel
|
|
self.afterClosed = afterClosed
|
|
}
|
|
|
|
public var body: some View {
|
|
Modal_SwiftUI(
|
|
host: host,
|
|
dismissType: dismissType,
|
|
afterClosed: afterClosed
|
|
) { close in
|
|
VStack(spacing: 0) {
|
|
// Background images
|
|
ZStack {
|
|
if let animatedAvatarImageURL = variant.animatedAvatarImageURL {
|
|
GeometryReader { geometry in
|
|
let size: CGFloat = geometry.size.width / 1522.0 * variant.animatedAvatarImageSize
|
|
let scale: CGFloat = geometry.size.width / 1522.0
|
|
SessionAsyncImage(
|
|
source: .url(animatedAvatarImageURL),
|
|
dataManager: dataManager,
|
|
shouldAnimateImage: true,
|
|
grayscale: variant.grayscale,
|
|
content: { image in
|
|
image
|
|
.resizable()
|
|
.aspectRatio(1, contentMode: .fit)
|
|
.frame(width: size, height: size)
|
|
},
|
|
placeholder: {
|
|
if let data = try? Data(contentsOf: animatedAvatarImageURL) {
|
|
Image(uiImage: UIImage(data: data) ?? UIImage())
|
|
.resizable()
|
|
.aspectRatio(1, contentMode: .fit)
|
|
.grayscale(variant.grayscale)
|
|
.frame(width: size, height: size)
|
|
} else {
|
|
EmptyView()
|
|
}
|
|
}
|
|
)
|
|
.padding(.leading, variant.animatedAvatarImagePadding.leading * scale)
|
|
.padding(.top, variant.animatedAvatarImagePadding.top * scale)
|
|
.onAppear {
|
|
proCTAImageHeight = geometry.size.width / 1522.0 * 1258.0
|
|
}
|
|
}
|
|
.frame(height: proCTAImageHeight)
|
|
}
|
|
|
|
Image(uiImage: UIImage(named: variant.backgroundImageName) ?? UIImage())
|
|
.resizable()
|
|
.aspectRatio((1522.0/1258.0), contentMode: .fit)
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
.backgroundColor(themeColor: variant.themeColor)
|
|
.overlay(alignment: .bottom, content: {
|
|
ThemeLinearGradient(
|
|
themeColors: [
|
|
.clear,
|
|
.alert_background
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
.framing(
|
|
maxWidth: .infinity,
|
|
height: 90
|
|
)
|
|
})
|
|
.frame(
|
|
maxWidth: .infinity,
|
|
alignment: .bottom
|
|
)
|
|
// Content
|
|
VStack(spacing: Values.largeSpacing) {
|
|
// Title
|
|
if variant.isRenewing {
|
|
HStack(spacing: Values.smallSpacing) {
|
|
Text("renew".localized())
|
|
.font(.Headings.H4)
|
|
.foregroundColor(themeColor: .textPrimary)
|
|
|
|
SessionProBadge_SwiftUI(size: .large)
|
|
}
|
|
} else {
|
|
switch variant {
|
|
case .animatedProfileImage(let isSessionProActivated, _) where isSessionProActivated:
|
|
HStack(spacing: Values.smallSpacing) {
|
|
SessionProBadge_SwiftUI(size: .large)
|
|
|
|
Text("proActivated".localized())
|
|
.font(.Headings.H4)
|
|
.foregroundColor(themeColor: .textPrimary)
|
|
}
|
|
|
|
case .groupLimit(_, let isSessionProActivated, _) where isSessionProActivated:
|
|
HStack(spacing: Values.smallSpacing) {
|
|
SessionProBadge_SwiftUI(size: .large)
|
|
|
|
Text("proGroupActivated".localized())
|
|
.font(.Headings.H4)
|
|
.foregroundColor(themeColor: .textPrimary)
|
|
}
|
|
|
|
case .expiring(let timeLeft):
|
|
let isExpired: Bool = (timeLeft?.isEmpty != false)
|
|
HStack(spacing: Values.smallSpacing) {
|
|
SessionProBadge_SwiftUI(
|
|
size: .large,
|
|
themeBackgroundColor: variant.themeColor
|
|
)
|
|
|
|
Text(isExpired ? "proExpired".localized() : "proExpiringSoon".localized())
|
|
.font(.Headings.H4)
|
|
.foregroundColor(themeColor: isExpired ? .disabled : .textPrimary)
|
|
}
|
|
|
|
default:
|
|
HStack(spacing: Values.smallSpacing) {
|
|
Text("upgradeTo".localized())
|
|
.font(.Headings.H4)
|
|
.foregroundColor(themeColor: .textPrimary)
|
|
|
|
SessionProBadge_SwiftUI(size: .large)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Description, Subtitle
|
|
VStack(spacing: 0) {
|
|
if case .animatedProfileImage(let isSessionProActivated, _) = variant, isSessionProActivated {
|
|
HStack(spacing: Values.verySmallSpacing) {
|
|
Text("proAlreadyPurchased".localized())
|
|
.font(.Body.largeRegular)
|
|
.foregroundColor(themeColor: .textSecondary)
|
|
|
|
SessionProBadge_SwiftUI(size: .medium)
|
|
}
|
|
}
|
|
|
|
if
|
|
case .groupLimit(_, let isSessionProActivated, let proBadgeImage) = variant,
|
|
isSessionProActivated
|
|
{
|
|
(Text(variant.subtitle(sessionProUIManager: sessionProUIManager).string) + Text(" \(Image(uiImage: proBadgeImage))"))
|
|
.font(.Body.largeRegular)
|
|
.foregroundColor(themeColor: .textSecondary)
|
|
.multilineTextAlignment(.center)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
} else {
|
|
AttributedText(variant.subtitle(sessionProUIManager: sessionProUIManager))
|
|
.font(.Body.largeRegular)
|
|
.foregroundColor(themeColor: .textSecondary)
|
|
.multilineTextAlignment(.center)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
}
|
|
|
|
// Benefits
|
|
if !variant.benefits.isEmpty {
|
|
VStack(alignment: .leading, spacing: Values.mediumSmallSpacing) {
|
|
ForEach(
|
|
0..<variant.benefits.count,
|
|
id: \.self
|
|
) { index in
|
|
HStack(spacing: Values.smallSpacing) {
|
|
if case .loadsMore = variant.benefits[index] {
|
|
CyclicGradientView {
|
|
AttributedText(Lucide.Icon.sparkles.attributedString(size: 17))
|
|
.font(.system(size: 17))
|
|
}
|
|
} else {
|
|
AttributedText(Lucide.Icon.circleCheck.attributedString(size: 17))
|
|
.font(.system(size: 17))
|
|
.foregroundColor(themeColor: .primary)
|
|
}
|
|
|
|
Text(variant.benefits[index].description)
|
|
.font(.Body.largeRegular)
|
|
.foregroundColor(themeColor: .textPrimary)
|
|
.frame(
|
|
maxWidth: .infinity,
|
|
alignment: .leading
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Buttons
|
|
if variant.onlyShowCloseButton {
|
|
GeometryReader { geometry in
|
|
HStack {
|
|
Button {
|
|
close(nil)
|
|
} label: {
|
|
Text(variant.confirmButtonTitle)
|
|
.font(.Body.baseRegular)
|
|
.foregroundColor(themeColor: .textPrimary)
|
|
}
|
|
.frame(
|
|
width: (geometry.size.width - Values.smallSpacing) / 2,
|
|
height: Values.largeButtonHeight
|
|
)
|
|
.backgroundColor(themeColor: .inputButton_background)
|
|
.cornerRadius(6)
|
|
.clipped()
|
|
.buttonStyle(PlainButtonStyle())
|
|
}
|
|
.frame(
|
|
width: geometry.size.width,
|
|
height: geometry.size.height,
|
|
alignment: .center
|
|
)
|
|
}
|
|
.frame(height: Values.largeButtonHeight)
|
|
} else {
|
|
HStack(spacing: Values.smallSpacing) {
|
|
// Upgrade Button
|
|
ShineButton {
|
|
close(onConfirm)
|
|
} label: {
|
|
Text(variant.confirmButtonTitle)
|
|
.font(.Body.baseRegular)
|
|
.foregroundColor(themeColor: .sessionButton_primaryFilledText)
|
|
.framing(
|
|
maxWidth: .infinity,
|
|
height: Values.largeButtonHeight
|
|
)
|
|
}
|
|
.frame(height: Values.largeButtonHeight)
|
|
.backgroundColor(themeColor: .sessionButton_primaryFilledBackground)
|
|
.cornerRadius(6)
|
|
.clipped()
|
|
.buttonStyle(PlainButtonStyle()) // prevents default blue highlight
|
|
|
|
// Cancel Button
|
|
Button {
|
|
onCancel?()
|
|
close(nil)
|
|
onCancel?()
|
|
} label: {
|
|
Text(variant.cancelButtonTitle)
|
|
.font(.Body.baseRegular)
|
|
.foregroundColor(themeColor: .textPrimary)
|
|
.framing(
|
|
maxWidth: .infinity,
|
|
height: Values.largeButtonHeight
|
|
)
|
|
}
|
|
.backgroundColor(themeColor: .inputButton_background)
|
|
.cornerRadius(6)
|
|
.clipped()
|
|
.buttonStyle(PlainButtonStyle())
|
|
}
|
|
}
|
|
}
|
|
.padding(Values.mediumSpacing)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - ProCTAModal.Benefits
|
|
|
|
public extension ProCTAModal {
|
|
enum Benefits: Equatable {
|
|
case largerGroups
|
|
case longerMessages
|
|
case animatedProfileImage
|
|
case morePinnedConvos
|
|
case loadsMore
|
|
|
|
var description: String {
|
|
return switch self {
|
|
case .largerGroups: "proFeatureListLargerGroups".localized()
|
|
case .longerMessages: "proFeatureListLongerMessages".localized()
|
|
case .animatedProfileImage: "proFeatureListAnimatedDisplayPicture".localized()
|
|
case .morePinnedConvos: "proFeatureListPinnedConversations".localized()
|
|
case .loadsMore: "proFeatureListLoadsMore".localized()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Variant Content
|
|
|
|
public extension ProCTAModal.Variant {
|
|
var isRenewing: Bool {
|
|
switch self {
|
|
case .generic(let renew), .longerMessages(let renew), .animatedProfileImage(_, let renew), .morePinnedConvos(_, let renew):
|
|
return renew
|
|
|
|
case .groupLimit, .expiring: return false
|
|
}
|
|
}
|
|
|
|
// stringlint:ignore_contents
|
|
var backgroundImageName: String {
|
|
switch self {
|
|
case .generic, .expiring: return "GenericCTA.webp"
|
|
case .longerMessages: return "HigherCharLimitCTA.webp"
|
|
case .animatedProfileImage: return "AnimatedProfileCTA.webp"
|
|
case .morePinnedConvos: return "PinnedConversationsCTA.webp"
|
|
case .groupLimit(false, false, _): return "GroupNonAdminCTA.webp"
|
|
case .groupLimit: return "GroupAdminCTA.webp"
|
|
}
|
|
}
|
|
|
|
var themeColor: ThemeValue {
|
|
switch self {
|
|
case .expiring(let timeLeft): return (timeLeft?.isEmpty == false ? .primary : .disabled)
|
|
default: return .primary
|
|
}
|
|
}
|
|
|
|
var grayscale: Double {
|
|
switch self {
|
|
case .expiring(let timeLeft): return (timeLeft?.isEmpty == false ? 0.0 : 1.0)
|
|
default: return 0.0
|
|
}
|
|
}
|
|
|
|
// stringlint:ignore_contents
|
|
var animatedAvatarImageURL: URL? {
|
|
switch self {
|
|
case .generic, .animatedProfileImage, .expiring:
|
|
return Bundle.main.url(forResource: "AnimatedProfileCTAAnimationCropped", withExtension: "webp")
|
|
default: return nil
|
|
}
|
|
}
|
|
|
|
/// Note: This is a hack to manually position the animated avatar in the CTA background image to prevent heavy loading for the
|
|
/// animated webp. These coordinates are based on the full size image and get scaled during rendering based on the actual size
|
|
/// of the modal.
|
|
var animatedAvatarImagePadding: (leading: CGFloat, top: CGFloat) {
|
|
switch self {
|
|
case .generic, .expiring: return (1303, 743)
|
|
case .animatedProfileImage: return (680, 363)
|
|
default: return (0, 0)
|
|
}
|
|
}
|
|
|
|
var animatedAvatarImageSize: CGFloat {
|
|
switch self {
|
|
case .generic, .expiring: return 115
|
|
case .animatedProfileImage: return 200
|
|
default: return 0
|
|
}
|
|
}
|
|
|
|
func subtitle(sessionProUIManager: SessionProUIManagerType) -> ThemedAttributedString {
|
|
switch self {
|
|
case .generic(renew: true):
|
|
return "proRenewMaxPotential"
|
|
.put(key: "pro", value: Constants.pro)
|
|
.put(key: "app_name", value: Constants.app_name)
|
|
.localizedFormatted(baseFont: Fonts.Body.largeRegular)
|
|
|
|
case .generic(renew: false):
|
|
return "proUserProfileModalCallToAction"
|
|
.put(key: "app_pro", value: Constants.app_pro)
|
|
.put(key: "app_name", value: Constants.app_name)
|
|
.localizedFormatted(baseFont: Fonts.Body.largeRegular)
|
|
|
|
case .longerMessages(renew: true):
|
|
return "proRenewLongerMessages"
|
|
.put(key: "pro", value: Constants.pro)
|
|
.localizedFormatted(baseFont: Fonts.Body.largeRegular)
|
|
|
|
case .longerMessages(renew: false):
|
|
return "proCallToActionLongerMessages"
|
|
.put(key: "app_pro", value: Constants.app_pro)
|
|
.localizedFormatted(baseFont: Fonts.Body.largeRegular)
|
|
|
|
case .animatedProfileImage(isSessionProActivated: true, _):
|
|
return "proAnimatedDisplayPicture"
|
|
.localizedFormatted(baseFont: Fonts.Body.largeRegular)
|
|
|
|
case .animatedProfileImage(isSessionProActivated: false, renew: true):
|
|
return "proRenewAnimatedDisplayPicture"
|
|
.put(key: "pro", value: Constants.pro)
|
|
.localizedFormatted(baseFont: Fonts.Body.largeRegular)
|
|
|
|
case .animatedProfileImage(isSessionProActivated: false, renew: false):
|
|
return "proAnimatedDisplayPictureCallToActionDescription"
|
|
.put(key: "app_pro", value: Constants.app_pro)
|
|
.localizedFormatted(baseFont: Fonts.Body.largeRegular)
|
|
|
|
case .morePinnedConvos(isGrandfathered: true, renew: false):
|
|
return "proCallToActionPinnedConversations"
|
|
.put(key: "app_pro", value: Constants.app_pro)
|
|
.localizedFormatted(baseFont: Fonts.Body.largeRegular)
|
|
|
|
case .morePinnedConvos(isGrandfathered: false, renew: false):
|
|
return "proCallToActionPinnedConversationsMoreThan"
|
|
.put(key: "app_pro", value: Constants.app_pro)
|
|
.put(key: "limit", value: sessionProUIManager.pinnedConversationLimit)
|
|
.localizedFormatted(baseFont: Fonts.Body.largeRegular)
|
|
|
|
case .morePinnedConvos(isGrandfathered: true, renew: true):
|
|
return "proRenewPinMoreConversations"
|
|
.put(key: "pro", value: Constants.pro)
|
|
.localizedFormatted(baseFont: Fonts.Body.largeRegular)
|
|
|
|
case .morePinnedConvos(isGrandfathered: false, renew: true):
|
|
return "proRenewPinFiveConversations"
|
|
.put(key: "pro", value: Constants.pro)
|
|
.put(key: "limit", value: sessionProUIManager.pinnedConversationLimit)
|
|
.localizedFormatted(baseFont: Fonts.Body.largeRegular)
|
|
|
|
case .groupLimit(_, isSessionProActivated: true, _):
|
|
return "proGroupActivatedDescription"
|
|
.localizedFormatted(baseFont: Fonts.Body.largeRegular)
|
|
|
|
case .groupLimit(isAdmin: true, isSessionProActivated: false, _):
|
|
return "proUserProfileModalCallToAction"
|
|
.put(key: "app_pro", value: Constants.app_pro)
|
|
.put(key: "app_name", value: Constants.app_name)
|
|
.localizedFormatted(baseFont: Fonts.Body.largeRegular)
|
|
|
|
case .groupLimit(isAdmin: false, isSessionProActivated: false, _):
|
|
// TODO: [PRO] Localised
|
|
return ThemedAttributedString(
|
|
string: "Want to upgrade this group to Pro? Tell one of the group admins to upgrade to Pro"
|
|
)
|
|
|
|
case .expiring(let timeLeft) where timeLeft?.isEmpty == false:
|
|
return "proExpiringSoonDescription"
|
|
.put(key: "pro", value: Constants.pro)
|
|
.put(key: "time", value: timeLeft ?? "")
|
|
.put(key: "app_pro", value: Constants.app_pro)
|
|
.localizedFormatted(baseFont: Fonts.Body.largeRegular)
|
|
|
|
case .expiring:
|
|
return "proExpiredDescription"
|
|
.put(key: "pro", value: Constants.pro)
|
|
.put(key: "app_pro", value: Constants.app_pro)
|
|
.localizedFormatted(baseFont: Fonts.Body.largeRegular)
|
|
}
|
|
}
|
|
|
|
var benefits: [ProCTAModal.Benefits] {
|
|
switch self {
|
|
case .generic: return [ .longerMessages, .morePinnedConvos, .loadsMore ]
|
|
case .longerMessages: return [ .longerMessages, .morePinnedConvos, .loadsMore ]
|
|
case .animatedProfileImage: return [ .animatedProfileImage, .longerMessages, .loadsMore ]
|
|
case .morePinnedConvos: return [ .morePinnedConvos, .longerMessages, .loadsMore ]
|
|
case .groupLimit(isAdmin: true, isSessionProActivated: false, _):
|
|
return [ .largerGroups, .longerMessages, .loadsMore ]
|
|
|
|
case .groupLimit: return []
|
|
case .expiring: return [ .longerMessages, .morePinnedConvos, .animatedProfileImage ]
|
|
}
|
|
}
|
|
|
|
var confirmButtonTitle: String {
|
|
switch self {
|
|
case .expiring(let timeLeft) where timeLeft?.isEmpty == false: return "update".localized()
|
|
case .expiring: return "renew".localized()
|
|
default: return "theContinue".localized()
|
|
}
|
|
}
|
|
|
|
var cancelButtonTitle: String {
|
|
guard !self.onlyShowCloseButton else {
|
|
return "close".localized()
|
|
}
|
|
|
|
switch self {
|
|
case .expiring(let timeLeft) where timeLeft?.isEmpty == false: return "close".localized()
|
|
case .expiring: return "cancel".localized()
|
|
default: return "cancel".localized()
|
|
}
|
|
}
|
|
|
|
var onlyShowCloseButton: Bool {
|
|
switch self {
|
|
case .animatedProfileImage(let isSessionProActivated, _): return isSessionProActivated
|
|
case .groupLimit(let isAdmin, let isSessionProActivated, _): return (!isAdmin || isSessionProActivated)
|
|
default: return false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Previews
|
|
|
|
struct ProCTAModal_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
Group {
|
|
PreviewThemeWrapper(theme: .classicDark) {
|
|
ProCTAModal(
|
|
variant: .generic(renew: false),
|
|
dataManager: ImageDataManager(),
|
|
sessionProUIManager: NoopSessionProUIManager(),
|
|
dismissType: .single,
|
|
afterClosed: nil
|
|
)
|
|
.environmentObject(HostWrapper())
|
|
.previewDisplayName("Classic Dark")
|
|
}
|
|
|
|
PreviewThemeWrapper(theme: .classicLight) {
|
|
ProCTAModal(
|
|
variant: .generic(renew: false),
|
|
dataManager: ImageDataManager(),
|
|
sessionProUIManager: NoopSessionProUIManager(),
|
|
dismissType: .single,
|
|
afterClosed: nil
|
|
)
|
|
.environmentObject(HostWrapper())
|
|
.previewDisplayName("Classic Light")
|
|
}
|
|
|
|
PreviewThemeWrapper(theme: .oceanDark) {
|
|
ProCTAModal(
|
|
variant: .generic(renew: false),
|
|
dataManager: ImageDataManager(),
|
|
sessionProUIManager: NoopSessionProUIManager(),
|
|
dismissType: .single,
|
|
afterClosed: nil
|
|
)
|
|
.environmentObject(HostWrapper())
|
|
.previewDisplayName("Ocean Dark")
|
|
}
|
|
|
|
PreviewThemeWrapper(theme: .oceanLight) {
|
|
ProCTAModal(
|
|
variant: .generic(renew: false),
|
|
dataManager: ImageDataManager(),
|
|
sessionProUIManager: NoopSessionProUIManager(),
|
|
dismissType: .single,
|
|
afterClosed: nil
|
|
)
|
|
.environmentObject(HostWrapper())
|
|
.previewDisplayName("Ocean Light")
|
|
}
|
|
}
|
|
}
|
|
}
|