WIP Very Good PayWall

This commit is contained in:
Kylmakalle 2025-02-09 01:13:01 +02:00
parent 9be7f38b5e
commit c47777aadc
12 changed files with 585 additions and 439 deletions

View File

@ -32,6 +32,7 @@ swift_library(
"//Swiftgram/SGSwiftUI:SGSwiftUI",
"//Swiftgram/SGIAP:SGIAP",
"//Swiftgram/SGPayWall:SGPayWall",
"//Swiftgram/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/LegacyUI:LegacyUI",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Postbox:Postbox",

View File

@ -10,6 +10,7 @@ import ItemListUI
import SwiftSignalKit
import TelegramPresentationData
import PresentationDataUtils
import TelegramUIPreferences
// Optional
import SGSimpleSettings
@ -648,8 +649,8 @@ public func sgSessionBackupManagerController(context: AccountContext, presentati
legacyController.title = "Session Backup" //i18n("BackupManager.Title", strings.baseLanguageCode)
let swiftUIView = SGSwiftUIView<SessionBackupManagerView>(
navigationBarHeight: legacyController.navigationBarHeightModel,
containerViewLayout: legacyController.containerViewLayoutModel,
legacyController: legacyController,
manageSafeArea: true,
content: {
SessionBackupManagerView(wrapperController: legacyController, context: context)
}
@ -984,7 +985,7 @@ public func sgDebugController(context: AccountContext) -> ViewController {
#if DEBUG
if #available(iOS 13.0, *) {
if let sgIAPManager = context.sharedContext.SGIAP {
presentControllerImpl?(sgPayWallController(presentationData: presentationData, SGIAPManager: sgIAPManager), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
presentControllerImpl?(sgPayWallController(accountManager: context.sharedContext.accountManager, presentationData: presentationData, SGIAPManager: sgIAPManager), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
} else {
presentControllerImpl?(UndoOverlayController(

View File

@ -100,6 +100,7 @@ public final class SGIAPManager: NSObject {
public private(set) var availableProducts: [SGProduct] = []
private var finishedSuccessfulTransactions = Set<String>()
private var onRestoreCompletion: (() -> Void)?
public final class SGProduct: Equatable {
private lazy var numberFormatter: NumberFormatter = {
@ -206,14 +207,21 @@ public final class SGIAPManager: NSObject {
super.init()
SKPaymentQueue.default().add(self)
#if DEBUG
DispatchQueue.main.asyncAfter(deadline: .now() + 30) {
self.requestProducts()
}
#else
self.requestProducts()
#endif
}
deinit {
SKPaymentQueue.default().remove(self)
}
var canMakePayments: Bool {
public var canMakePayments: Bool {
return SKPaymentQueue.canMakePayments()
}
@ -233,17 +241,14 @@ public final class SGIAPManager: NSObject {
self.productRequest = productRequest
}
public func restorePurchases(completion: @escaping (RestoreState) -> Void) {
public func restorePurchases(completion: @escaping () -> Void) {
SGLogger.shared.log("SGIAP", "Restoring purchases...")
self.onRestoreCompletion = completion
let paymentQueue = SKPaymentQueue.default()
paymentQueue.restoreCompletedTransactions()
}
public enum RestoreState {
case succeed(Bool)
case failed
}
}
extension SGIAPManager: SKProductsRequestDelegate {
@ -292,6 +297,15 @@ extension SGIAPManager: SKPaymentTransactionObserver {
}
}
}
public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
SGLogger.shared.log("SGIAP", "Transactions restored")
if let onRestoreCompletion = self.onRestoreCompletion {
self.onRestoreCompletion = nil
onRestoreCompletion()
}
}
}

View File

@ -1,5 +1,11 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
filegroup(
name = "SGPayWallAssets",
srcs = glob(["Images.xcassets/**"]),
visibility = ["//visibility:public"],
)
swift_library(
name = "SGPayWall",
module_name = "SGPayWall",

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "pro.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "pro@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "pro@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -6,130 +6,13 @@ import TelegramPresentationData
import LegacyUI
import Display
import SGConfig
// import SGStrings
struct SGPerk: Identifiable {
let id = UUID()
let title: String
let description: String
let icon: String
}
import SGStrings
import SwiftSignalKit
import TelegramUIPreferences
@available(iOS 13.0, *)
struct SGPayWallView: View {
weak var wrapperController: LegacyController?
let SGIAP: SGIAPManager
let perks = [
SGPerk(title: "Premium Features", description: "Access all premium features and tools", icon: "star.fill"),
SGPerk(title: "No Ads", description: "Enjoy an ad-free experience", icon: "banner.slash"),
SGPerk(title: "Cloud Sync", description: "Sync your data across all devices", icon: "cloud.fill"),
SGPerk(title: "Priority Support", description: "Get priority customer support", icon: "questionmark.circle.fill"),
SGPerk(title: "Advanced Stats", description: "Access detailed analytics and insights", icon: "chart.bar.fill"),
SGPerk(title: "Premium Features", description: "Access all premium features and tools", icon: "star.fill"),
SGPerk(title: "No Ads", description: "Enjoy an ad-free experience", icon: "banner.slash"),
SGPerk(title: "Cloud Sync", description: "Sync your data across all devices", icon: "cloud.fill"),
SGPerk(title: "Priority Support", description: "Get priority customer support", icon: "questionmark.circle.fill"),
SGPerk(title: "Advanced Stats", description: "Access detailed analytics and insights", icon: "chart.bar.fill"),
SGPerk(title: "Advanced Stats", description: "Access detailed analytics and insights", icon: "chart.bar.fill"),
SGPerk(title: "Premium Features", description: "Access all premium features and tools", icon: "star.fill"),
SGPerk(title: "No Ads", description: "Enjoy an ad-free experience", icon: "banner.slash"),
SGPerk(title: "Cloud Sync", description: "Sync your data across all devices", icon: "cloud.fill"),
SGPerk(title: "Priority Support", description: "Get priority customer support", icon: "questionmark.circle.fill"),
SGPerk(title: "Advanced Stats", description: "Access detailed analytics and insights", icon: "chart.bar.fill"),
]
var body: some View {
NavigationView {
VStack(spacing: 0) {
ScrollView {
VStack(spacing: 24) {
ForEach(perks) { perk in
HStack(spacing: 16) {
Image(systemName: perk.icon)
.font(.title)
.foregroundColor(.blue)
.frame(width: 32)
VStack(alignment: .leading, spacing: 4) {
Text(perk.title)
.font(.headline)
Text(perk.description)
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
}
.padding(.horizontal)
}
}
.padding(.vertical, 24)
}
VStack(spacing: 12) {
Button(action: {
for availableProduct in SGIAP.availableProducts {
if SG_CONFIG.iaps.contains(availableProduct.skProduct.productIdentifier ) {
SGIAP.purchaseProduct(availableProduct, completion: { _ in })
}
}
SGIAP.buyProduct(product: product)
}) {
Text("Unlock Premium - $9.99")
.font(.headline)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.cornerRadius(16)
}
Button(action: {
SGIAP.restorePurchases(completion: { _ in })
}) {
Text("Restore Purchases")
.font(.subheadline)
.foregroundColor(.blue)
}
}
.padding(24)
.background(
Rectangle()
.fill(Color(UIColor.systemBackground))
.shadow(radius: 8, y: -4)
)
}.navigationBarItems(trailing: closeButtonView)
}
.colorScheme(.dark)
}
private var closeButtonView: some View {
Button(action: {
wrapperController?.dismiss(animated: true)
}) {
if #available(iOS 15.0, *) {
Image(systemName: "xmark.circle.fill")
.font(.headline)
.symbolRenderingMode(.hierarchical)
.foregroundColor(.secondary)
} else {
Image(systemName: "xmark.circle.fill")
.font(.headline)
.foregroundColor(.secondary)
}
}
}
}
@available(iOS 13.0, *)
public func sgPayWallController(presentationData: PresentationData? = nil, SGIAPManager: SGIAPManager) -> ViewController {
public func sgPayWallController(accountManager: AccountMana presentationData: PresentationData? = nil, SGIAPManager: SGIAPManager) -> ViewController {
// let theme = presentationData?.theme ?? (UITraitCollection.current.userInterfaceStyle == .dark ? defaultDarkColorPresentationTheme : defaultPresentationTheme)
let theme = defaultDarkColorPresentationTheme
let strings = presentationData?.strings ?? defaultPresentationStrings
@ -142,296 +25,352 @@ public func sgPayWallController(presentationData: PresentationData? = nil, SGIAP
// legacyController.displayNavigationBar = false
legacyController.statusBar.statusBarStyle = .White
legacyController.attemptNavigation = { _ in return false }
let swiftUIView = SGPayWallView(wrapperController: legacyController, SGIAP: SGIAPManager)
let statusSignal = accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.sgStatus])
|> take(1)
|> map { sharedData -> Int64 in
if let sgStatus = sharedData.entries[ApplicationSpecificSharedDataKeys.sgStatus] as? SGStatus {
return sgStatus.status
} else {
return SGStatus.default
}
}
let swiftUIView = SGSwiftUIView<SGPayWallView>(
legacyController: legacyController,
content: {
SGPayWallView(statusSignal: statusSignal, wrapperController: legacyController, SGIAP: SGIAPManager, lang: strings.baseLanguageCode)
}
)
let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true)
legacyController.bind(controller: controller)
return legacyController
}
//
//import SwiftUI
//
//
//struct ModalView: View {
// @State private var isShowingModal = true
//
// var body: some View {
// Button("Show Modal") {
// isShowingModal.toggle()
// }
// .sheet(isPresented: $isShowingModal) {
// ContentView()
// .presentationDetents([.large]) // iOS 16+
// .presentationDragIndicator(.hidden)
// }
// }
//}
//
//let innerShadowWidth: CGFloat = 15.0
//
//struct BackgroundView: View {
// var body: some View {
// ZStack {
// LinearGradient(
// gradient: Gradient(stops: [
// .init(color: Color(hex: "A053F8").opacity(0.8), location: 0.0),
// .init(color: Color.clear, location: 0.20),
//
// ]),
// startPoint: .topLeading,
// endPoint: .bottomTrailing
// )
// .edgesIgnoringSafeArea(.all)
// LinearGradient(
// gradient: Gradient(stops: [
// .init(color: Color(hex: "CC4303").opacity(0.6), location: 0.0),
// .init(color: Color.clear, location: 0.15),
// ]),
// startPoint: .topTrailing,
// endPoint: .bottomLeading
// )
// .blendMode(.lighten)
//
// .edgesIgnoringSafeArea(.all)
// .overlay(
// RoundedRectangle(cornerRadius: 0)
// .stroke(Color.clear, lineWidth: 0)
// .background(
// ZStack {
// innerShadow(x: -2, y: -2, blur: 4, color: Color(hex: "FF8C56")) // orange shadow
// innerShadow(x: 2, y: 2, blur: 4, color: Color(hex: "A053F8")) // purple shadow
// // innerShadow(x: 0, y: 0, blur: 4, color: Color.white.opacity(0.3))
// }
// )
// ).ignoresSafeArea(.all)
// }
// .background(Color.black)
// }
//
// func innerShadow(x: CGFloat, y: CGFloat, blur: CGFloat, color: Color) -> some View {
// return RoundedRectangle(cornerRadius: 0)
// .stroke(color, lineWidth: innerShadowWidth)
// .blur(radius: blur)
// .offset(x: x, y: y)
// .mask(RoundedRectangle(cornerRadius: 0).fill(LinearGradient(gradient: Gradient(colors: [Color.black, Color.clear]), startPoint: .top, endPoint: .bottom)))
// }
//}
//
//extension Color {
// init(hex: String) {
// let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
// var int: UInt64 = 0
// Scanner(string: hex).scanHexInt64(&int)
// let a, r, g, b: UInt64
// switch hex.count {
// case 6: // RGB (No alpha)
// (a, r, g, b) = (255, (int >> 16) & 0xff, (int >> 8) & 0xff, int & 0xff)
// case 8: // ARGB
// (a, r, g, b) = ((int >> 24) & 0xff, (int >> 16) & 0xff, (int >> 8) & 0xff, int & 0xff)
// default:
// (a, r, g, b) = (255, 0, 0, 0)
// }
// self.init(.sRGB, red: Double(r) / 255, green: Double(g) / 255, blue: Double(b) / 255, opacity: Double(a) / 255)
// }
//}
//
//
//
//struct ContentView: View {
// var body: some View {
// bodyContent
// .overlay(closeButtonView
// .padding([.top, .trailing], 16) // Add padding from top and right edges
// .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
// )
// }
//
// var bodyContent: some View {
// ZStack {
// BackgroundView()
// ZStack(alignment: .bottom) {
// ScrollView(showsIndicators: false) {
// VStack(spacing: 24) {
// // Premium Icon
// PremiumIconView()
// .frame(width: 100, height: 100)
//
// // Title and Subtitle
// VStack(spacing: 8) {
// Text("Swiftgram Pro")
// .font(.largeTitle)
// .fontWeight(.bold)
//
// Text("Supercharged with Pro features")
// .font(.callout)
// // .foregroundColor(.secondary)
// .multilineTextAlignment(.center)
// .padding(.horizontal)
// }
//
// // Features List
// VStack(spacing: 8) {
// FeatureRow(
// icon: FeatureIcon(icon: "lock.fill", backgroundColor: .blue),
// title: "Session Backup",
// subtitle: "Restore sessions from encrypted local Apple Keychain backup."
// )
//
// FeatureRow(
// icon: FeatureIcon(icon: "nosign", backgroundColor: .gray, fontWeight: .bold),
// title: "Message Filter",
// subtitle: "Reduce visibility of spam, promotions and annoying messages."
// )
//
// FeatureRow(
// icon: FeatureIcon(icon: "bell.badge.slash.fill", backgroundColor: .red),
// title: "Disable @mentions and replies",
// subtitle: "Hide or silence non-important notifications."
// )
//
// FeatureRow(
// icon: FeatureIcon(icon: "bold.underline", backgroundColor: .blue, iconSize: 16),
// title: "Quick Formatting panel",
// subtitle: "Save time preparing your posts with a panel right above your keyboard."
// )
//
//
// }
// .padding(.horizontal, innerShadowWidth + 8.0)
//
// Text("Privacy Policy")
// .font(.footnote)
//
// Color.clear.frame(height: 40)
// }
// .padding(.vertical, 40)
// }
// // Fixed purchase button at bottom
// VStack(spacing: 0) {
// Divider()
//
// Button(action: {
// // Your purchase action here
// }) {
// Text("Subscribe for $9.99 / month")
// .fontWeight(.semibold)
// .frame(maxWidth: .infinity)
// .padding()
// .background(Color(hex: "F1552E"))
// .foregroundColor(.white)
// .cornerRadius(12)
// }
// .padding()
// }
// .foregroundColor(Color.black)
// .background(.ultraThinMaterial)
// .shadow(radius: 8, y: -4)
// }
// }
// .colorScheme(.dark)
// }
//
// private var closeButtonView: some View {
// Button(action: {
//
// }) {
// Image(systemName: "xmark")
// .font(.headline)
// .foregroundColor(.secondary.opacity(0.6))
// }
// }
//}
//
//// Premium Icon View using SVG
//struct PremiumIconView: View {
// var body: some View {
// Image("PRO") // You'll need to add the SVG to your assets
// }
//}
//
//struct FeatureIcon: View {
// let icon: String
// let iconColor: Color
// let backgroundColor: Color
// let iconSize: CGFloat
// let frameSize: CGFloat
// let fontWeight: Font.Weight
//
// init(
// icon: String,
// iconColor: Color = .white,
// backgroundColor: Color = .blue,
// iconSize: CGFloat = 18,
// frameSize: CGFloat = 32,
// fontWeight: Font.Weight = .regular
// ) {
// self.icon = icon
// self.iconColor = iconColor
// self.backgroundColor = backgroundColor
// self.iconSize = iconSize
// self.frameSize = frameSize
// self.fontWeight = fontWeight
// }
//
// var body: some View {
// Image(systemName: icon)
// .font(.system(size: iconSize))
// .fontWeight(fontWeight)
// .foregroundColor(iconColor)
// .frame(width: frameSize, height: frameSize)
// .background(backgroundColor)
// .clipShape(RoundedRectangle(cornerRadius: 8))
// }
//}
//
//
//// Feature Row Component
//struct FeatureRow: View {
// let icon: FeatureIcon
// let title: String
// let subtitle: String
//
// var body: some View {
// Button(action: {
// // Add your tap action here
// }) {
// HStack(spacing: 16) {
//
// HStack(alignment: .top, spacing: 12) {
// icon
//
// VStack(alignment: .leading, spacing: 4) {
// Text(title)
// .font(.headline)
// .fontWeight(.medium)
//
// Text(subtitle)
// .font(.subheadline)
// .foregroundColor(.secondary)
// }
// }
//
// Spacer()
//
// Image(systemName: "chevron.right")
// .font(.system(size: 12, weight: .semibold))
// .foregroundColor(.secondary)
// }
// .padding()
// .background(
// RoundedRectangle(cornerRadius: 12)
// .fill(Color(.systemGray6))
// .shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 4)
// )
// }
// .buttonStyle(PlainButtonStyle())
// }
//}
//
//
//struct ContentView_Previews: PreviewProvider {
// static var previews: some View {
// ModalView()
// }
//}
private let innerShadowWidth: CGFloat = 15.0
private let accentColorHex: String = "F1552E"
@available(iOS 13.0, *)
struct BackgroundView: View {
var body: some View {
ZStack {
LinearGradient(
gradient: Gradient(stops: [
.init(color: Color(hex: "A053F8").opacity(0.8), location: 0.0), // purple gradient
.init(color: Color.clear, location: 0.20),
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.edgesIgnoringSafeArea(.all)
LinearGradient(
gradient: Gradient(stops: [
.init(color: Color(hex: "CC4303").opacity(0.6), location: 0.0), // orange gradient
.init(color: Color.clear, location: 0.15),
]),
startPoint: .topTrailing,
endPoint: .bottomLeading
)
.blendMode(.lighten)
.edgesIgnoringSafeArea(.all)
.overlay(
RoundedRectangle(cornerRadius: 0)
.stroke(Color.clear, lineWidth: 0)
.background(
ZStack {
innerShadow(x: -2, y: -2, blur: 4, color: Color(hex: "FF8C56")) // orange shadow
innerShadow(x: 2, y: 2, blur: 4, color: Color(hex: "A053F8")) // purple shadow
// innerShadow(x: 0, y: 0, blur: 4, color: Color.white.opacity(0.3))
}
)
)
.edgesIgnoringSafeArea(.all)
}
.background(Color.black)
}
func innerShadow(x: CGFloat, y: CGFloat, blur: CGFloat, color: Color) -> some View {
return RoundedRectangle(cornerRadius: 0)
.stroke(color, lineWidth: innerShadowWidth)
.blur(radius: blur)
.offset(x: x, y: y)
.mask(RoundedRectangle(cornerRadius: 0).fill(LinearGradient(gradient: Gradient(colors: [Color.black, Color.clear]), startPoint: .top, endPoint: .bottom)))
}
}
@available(iOS 13.0, *)
struct SGPayWallView: View {
@Environment(\.navigationBarHeight) var navigationBarHeight: CGFloat
@Environment(\.containerViewLayout) var containerViewLayout: ContainerViewLayout?
weak var wrapperController: LegacyController?
let SGIAP: SGIAPManager
let lang: String
// State management
@State private var product: SGIAPManager.SGProduct?
@State private var isRestoringPurchases = false
private let productsPub = NotificationCenter.default.publisher(for: .SGIAPHelperProductsUpdatedNotification, object: nil)
// Loading state enum
private enum LoadingState {
case loading
case loaded
case error(String)
}
var body: some View {
ZStack {
BackgroundView()
ZStack(alignment: .bottom) {
ScrollView(showsIndicators: false) {
VStack(spacing: 24) {
// Icon
Image("pro")
.frame(width: 100, height: 100)
// Title and Subtitle
VStack(spacing: 8) {
Text("Swiftgram Pro")
.font(.largeTitle)
.fontWeight(.bold)
Text("Supercharged with Pro features")
.font(.callout)
.multilineTextAlignment(.center)
.padding(.horizontal)
}
// Features
VStack(spacing: 8) {
featuresSection
restorePurchasesButton
}
// Spacer for purchase buttons
Color.clear.frame(height: 50)
}
.padding(.vertical, 50)
}
// Fixed purchase button at bottom
purchaseSection
}
}
.overlay(closeButtonView)
.colorScheme(.dark)
.onReceive(productsPub) { _ in
updateSelectedProduct()
}
.onAppear {
updateSelectedProduct()
}
}
private var featuresSection: some View {
VStack(spacing: 8) {
FeatureRow(
icon: FeatureIcon(icon: "lock.fill", backgroundColor: .blue),
title: "Session Backup",
subtitle: "Restore sessions from encrypted local Apple Keychain backup."
)
FeatureRow(
icon: FeatureIcon(icon: "nosign", backgroundColor: .gray, fontWeight: .bold),
title: "Message Filter",
subtitle: "Reduce visibility of spam, promotions and annoying messages."
)
FeatureRow(
icon: FeatureIcon(icon: "bell.badge.slash.fill", backgroundColor: .red),
title: "Disable @mentions and replies",
subtitle: "Hide or silence non-important notifications."
)
FeatureRow(
icon: FeatureIcon(icon: "bold.underline", backgroundColor: .blue, iconSize: 16),
title: "Quick Formatting panel",
subtitle: "Save time preparing your posts with a panel right above your keyboard."
)
}
.padding(.leading, max(innerShadowWidth + 8.0, sgLeftSafeAreaInset(containerViewLayout)))
.padding(.trailing, max(innerShadowWidth + 8.0, sgRightSafeAreaInset(containerViewLayout)))
}
private var restorePurchasesButton: some View {
Button(action: handleRestorePurchases) {
Text("Restore Purchases")
.font(.footnote)
.fontWeight(.semibold)
.foregroundColor(Color(hex: accentColorHex))
}
.disabled(isRestoringPurchases)
.opacity(isRestoringPurchases ? 1.0 : 0.5)
}
private var purchaseSection: some View {
VStack(spacing: 0) {
Divider()
Button(action: handlePurchase) {
Text(buttonTitle)
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
.padding()
.background(Color(hex: accentColorHex))
.foregroundColor(.white)
.cornerRadius(12)
}
.disabled(!canPurchase)
.opacity(canPurchase ? 1.0 : 0.5)
.padding([.horizontal, .top])
.padding(.bottom, sgBottomSafeAreaInset(containerViewLayout))
}
.foregroundColor(Color.black)
.backgroundIfAvailable(material: .ultraThinMaterial)
.shadow(radius: 8, y: -4)
}
private var closeButtonView: some View {
Button(action: dismiss) {
Image(systemName: "xmark")
.font(.headline)
.foregroundColor(.secondary.opacity(0.6))
}
.padding([.top, .trailing], 16)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
}
// MARK: - Computed Properties
private var buttonTitle: String {
if let product = product {
if !SGIAP.canMakePayments {
return "Payments unavailable"
} else {
return "Subscribe for \(product.price) / month"
}
} else {
return "Contacting App Store..."
}
}
private var canPurchase: Bool {
if !SGIAP.canMakePayments {
return false
} else {
return product != nil
}
}
// MARK: - Methods
private func updateSelectedProduct() {
product = SGIAP.availableProducts.first { $0.id == SG_CONFIG.iaps.first ?? "" }
}
private func handlePurchase() {
guard let product = product else { return }
SGIAP.buyProduct(product.skProduct)
}
private func handleRestorePurchases() {
isRestoringPurchases = true
SGIAP.restorePurchases {
isRestoringPurchases = false
}
}
private func dismiss() {
wrapperController?.dismiss(animated: true)
}
}
@available(iOS 13.0, *)
struct FeatureIcon: View {
let icon: String
let iconColor: Color
let backgroundColor: Color
let iconSize: CGFloat
let frameSize: CGFloat
let fontWeight: SwiftUI.Font.Weight
init(
icon: String,
iconColor: Color = .white,
backgroundColor: Color = .blue,
iconSize: CGFloat = 18,
frameSize: CGFloat = 32,
fontWeight: SwiftUI.Font.Weight = .regular
) {
self.icon = icon
self.iconColor = iconColor
self.backgroundColor = backgroundColor
self.iconSize = iconSize
self.frameSize = frameSize
self.fontWeight = fontWeight
}
var body: some View {
Image(systemName: icon)
.font(.system(size: iconSize))
.fontWeightIfAvailable(fontWeight)
.foregroundColor(iconColor)
.frame(width: frameSize, height: frameSize)
.background(backgroundColor)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
@available(iOS 13.0, *)
struct FeatureRow: View {
let icon: FeatureIcon
let title: String
let subtitle: String
var body: some View {
Button(action: {
// TODO(swiftgram): Feature row clarification
}) {
HStack(spacing: 16) {
HStack(alignment: .top, spacing: 12) {
icon
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.headline)
.fontWeight(.medium)
Text(subtitle)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .semibold))
.foregroundColor(.secondary)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color(.systemGray6))
.shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 4)
)
}
.buttonStyle(PlainButtonStyle())
}
}

View File

@ -7,75 +7,170 @@ import TelegramPresentationData
@available(iOS 13.0, *)
public class ObservedValue<T>: ObservableObject {
@Published var value: T
@Published public var value: T
init(_ value: T) {
public init(_ value: T) {
self.value = value
}
}
@available(iOS 13.0, *)
public struct NavigationBarHeightKey: EnvironmentKey {
public static let defaultValue: CGFloat = 0
}
@available(iOS 13.0, *)
public struct ContainerViewLayoutKey: EnvironmentKey {
public static let defaultValue: ContainerViewLayout? = nil
}
// Perhaps, affects Performance a lot
//@available(iOS 13.0, *)
//public struct ContainerViewLayoutUpdateCountKey: EnvironmentKey {
// public static let defaultValue: ObservedValue<Int64> = ObservedValue(0)
//}
@available(iOS 13.0, *)
public extension EnvironmentValues {
var navigationBarHeight: CGFloat {
get { self[NavigationBarHeightKey.self] }
set { self[NavigationBarHeightKey.self] = newValue }
}
var containerViewLayout: ContainerViewLayout? {
get { self[ContainerViewLayoutKey.self] }
set { self[ContainerViewLayoutKey.self] = newValue }
}
// var containerViewLayoutUpdateCount: ObservedValue<Int64> {
// get { self[ContainerViewLayoutUpdateCountKey.self] }
// set { self[ContainerViewLayoutUpdateCountKey.self] = newValue }
// }
}
@available(iOS 13.0, *)
public struct SGSwiftUIView<Content: View>: View {
public let content: Content
public let manageSafeArea: Bool
@ObservedObject var navigationBarHeight: ObservedValue<CGFloat>
@ObservedObject var containerViewLayout: ObservedValue<ContainerViewLayout?>
// @ObservedObject var containerViewLayoutUpdateCount: ObservedValue<Int64>
public init(
navigationBarHeight: ObservedValue<CGFloat>,
containerViewLayout: ObservedValue<ContainerViewLayout?>,
legacyController: LegacySwiftUIController,
manageSafeArea: Bool = false,
@ViewBuilder content: () -> Content
) {
self.navigationBarHeight = navigationBarHeight
self.containerViewLayout = containerViewLayout
#if DEBUG
if manageSafeArea {
print("WARNING SGSwiftUIView: manageSafeArea is deprecated, use @Environment(\\.navigationBarHeight) and @Environment(\\.containerViewLayout)")
}
#endif
self.navigationBarHeight = legacyController.navigationBarHeightModel
self.containerViewLayout = legacyController.containerViewLayoutModel
// self.containerViewLayoutUpdateCount = legacyController.containerViewLayoutUpdateCountModel
self.manageSafeArea = manageSafeArea
self.content = content()
}
public var body: some View {
content
.modifier(CustomSafeAreaPadding(navigationBarHeight: navigationBarHeight, containerViewLayout: containerViewLayout))
.if(manageSafeArea) { $0.modifier(CustomSafeArea()) }
.environment(\.navigationBarHeight, navigationBarHeight.value)
.environment(\.containerViewLayout, containerViewLayout.value)
// .environment(\.containerViewLayoutUpdateCount, containerViewLayoutUpdateCount)
// .onReceive(containerViewLayoutUpdateCount.$value) { _ in
// // Make sure View is updated when containerViewLayoutUpdateCount changes,
// // in case it does not depend on containerViewLayout
// }
}
}
@available(iOS 13.0, *)
public struct CustomSafeAreaPadding: ViewModifier {
@ObservedObject var navigationBarHeight: ObservedValue<CGFloat>
@ObservedObject var containerViewLayout: ObservedValue<ContainerViewLayout?>
public struct CustomSafeArea: ViewModifier {
@Environment(\.navigationBarHeight) var navigationBarHeight: CGFloat
@Environment(\.containerViewLayout) var containerViewLayout: ContainerViewLayout?
public func body(content: Content) -> some View {
content
.edgesIgnoringSafeArea(.all)
// .padding(.top, /*totalTopSafeArea > navigationBarHeight.value ? totalTopSafeArea :*/ navigationBarHeight.value)
.padding(.top, totalTopSafeArea > navigationBarHeight.value ? totalTopSafeArea : navigationBarHeight.value)
.padding(.bottom, (containerViewLayout.value?.safeInsets.bottom ?? 0) /*+ (containerViewLayout.value?.intrinsicInsets.bottom ?? 0)*/)
.padding(.leading, containerViewLayout.value?.safeInsets.left ?? 0)
.padding(.trailing, containerViewLayout.value?.safeInsets.right ?? 0)
.padding(.top, topInset)
.padding(.bottom, bottomInset)
.padding(.leading, leftInset)
.padding(.trailing, rightInset)
}
var totalTopSafeArea: CGFloat {
(containerViewLayout.value?.safeInsets.top ?? 0) +
(containerViewLayout.value?.intrinsicInsets.top ?? 0)
private var topInset: CGFloat {
max(
(containerViewLayout?.safeInsets.top ?? 0) + (containerViewLayout?.intrinsicInsets.top ?? 0),
navigationBarHeight
)
}
private var bottomInset: CGFloat {
(containerViewLayout?.safeInsets.bottom ?? 0)
// DEPRECATED, do not change
// + (containerViewLayout.value?.intrinsicInsets.bottom ?? 0)
}
private var leftInset: CGFloat {
containerViewLayout?.safeInsets.left ?? 0
}
private var rightInset: CGFloat {
containerViewLayout?.safeInsets.right ?? 0
}
}
@available(iOS 13.0, *)
public extension View {
func sgTopSafeAreaInset(_ containerViewLayout: ContainerViewLayout?, _ navigationBarHeight: CGFloat) -> CGFloat {
return max(
(containerViewLayout?.safeInsets.top ?? 0) + (containerViewLayout?.intrinsicInsets.top ?? 0),
navigationBarHeight
)
}
func sgBottomSafeAreaInset(_ containerViewLayout: ContainerViewLayout?) -> CGFloat {
return (containerViewLayout?.safeInsets.bottom ?? 0) + (containerViewLayout?.intrinsicInsets.bottom ?? 0)
}
func sgLeftSafeAreaInset(_ containerViewLayout: ContainerViewLayout?) -> CGFloat {
return containerViewLayout?.safeInsets.left ?? 0
}
func sgRightSafeAreaInset(_ containerViewLayout: ContainerViewLayout?) -> CGFloat {
return containerViewLayout?.safeInsets.right ?? 0
}
}
@available(iOS 13.0, *)
public final class LegacySwiftUIController: LegacyController {
public var navigationBarHeightModel: ObservedValue<CGFloat>
public var containerViewLayoutModel: ObservedValue<ContainerViewLayout?>
public var inputHeightModel: ObservedValue<CGFloat?>
// public var containerViewLayoutUpdateCountModel: ObservedValue<Int64>
override public init(presentation: LegacyControllerPresentation, theme: PresentationTheme? = nil, strings: PresentationStrings? = nil, initialLayout: ContainerViewLayout? = nil) {
navigationBarHeightModel = ObservedValue<CGFloat>(0.0)
containerViewLayoutModel = ObservedValue<ContainerViewLayout?>(initialLayout)
inputHeightModel = ObservedValue<CGFloat?>(nil)
// containerViewLayoutUpdateCountModel = ObservedValue<Int64>(0)
super.init(presentation: presentation, theme: theme, strings: strings, initialLayout: initialLayout)
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
// containerViewLayoutUpdateCountModel.value += 1
var newNavigationBarHeight = navigationLayout(layout: layout).navigationFrame.maxY
if !self.displayNavigationBar {
if !self.displayNavigationBar || self.navigationPresentation == .modal {
newNavigationBarHeight = 0.0
}
if navigationBarHeightModel.value != newNavigationBarHeight {
@ -237,7 +332,7 @@ public extension View {
}
@available(iOS 13.0, *)
extension Color {
public extension Color {
func uiColor() -> UIColor {
@ -264,4 +359,65 @@ extension Color {
}
return (r, g, b, a)
}
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 6: // RGB (No alpha)
(a, r, g, b) = (255, (int >> 16) & 0xff, (int >> 8) & 0xff, int & 0xff)
case 8: // ARGB
(a, r, g, b) = ((int >> 24) & 0xff, (int >> 16) & 0xff, (int >> 8) & 0xff, int & 0xff)
default:
(a, r, g, b) = (255, 0, 0, 0)
}
self.init(.sRGB, red: Double(r) / 255, green: Double(g) / 255, blue: Double(b) / 255, opacity: Double(a) / 255)
}
}
public enum BackgroundMaterial {
case ultraThinMaterial
case thinMaterial
case regularMaterial
case thickMaterial
case ultraThickMaterial
@available(iOS 15.0, *)
var material: Material {
switch self {
case .ultraThinMaterial: return .ultraThinMaterial
case .thinMaterial: return .thinMaterial
case .regularMaterial: return .regularMaterial
case .thickMaterial: return .thickMaterial
case .ultraThickMaterial: return .ultraThickMaterial
}
}
}
@available(iOS 13.0, *)
public extension View {
func fontWeightIfAvailable(_ weight: SwiftUI.Font.Weight) -> some View {
if #available(iOS 16.0, *) {
return self.fontWeight(weight)
} else {
return self
}
}
func backgroundIfAvailable(material: BackgroundMaterial) -> some View {
if #available(iOS 15.0, *) {
return self.background(material.material)
} else {
return self.background(
Color(.systemBackground)
.opacity(0.75)
.blur(radius: 3)
.overlay(Color.white.opacity(0.1))
)
}
}
}

View File

@ -365,7 +365,7 @@ objc_library(
],
)
SGRESOURCES = ["//Swiftgram/SGSettingsUI:SGUIAssets"]
SGRESOURCES = ["//Swiftgram/SGSettingsUI:SGUIAssets", "//Swiftgram/SGPayWall:SGPayWallAssets"]
swift_library(
name = "Lib",