Pro feature details

This commit is contained in:
Kylmakalle 2025-03-16 16:41:41 +02:00
parent f21ebab517
commit 74ca4ab8a0
17 changed files with 556 additions and 72 deletions

View File

@ -159,17 +159,14 @@ public func sgDebugController(context: AccountContext) -> ViewController {
} }
#endif #endif
case .restorePurchases: case .restorePurchases:
context.sharedContext.SGIAP?.restorePurchases { presentControllerImpl?(UndoOverlayController(
DispatchQueue.main.async { presentationData: presentationData,
presentControllerImpl?(UndoOverlayController( content: .info(title: nil, text: "PayWall.Button.Restoring".i18n(args: context.sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode), timeout: nil, customUndoText: nil),
presentationData: presentationData, elevatedLayout: false,
content: .info(title: nil, text: "PayWall.Button.Restoring".i18n(args: context.sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode), timeout: nil, customUndoText: nil), action: { _ in return false }
elevatedLayout: false, ),
action: { _ in return false } nil)
), context.sharedContext.SGIAP?.restorePurchases {}
nil)
}
}
case .setIAP: case .setIAP:
#if DEBUG #if DEBUG
#endif #endif

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 KiB

View File

@ -13,7 +13,7 @@ import TelegramUIPreferences
@available(iOS 13.0, *) @available(iOS 13.0, *)
public func sgPayWallController(statusSignal: Signal<Int64, NoError>, replacementController: ViewController, presentationData: PresentationData? = nil, SGIAPManager: SGIAPManager, openUrl: @escaping (String) -> Void, paymentsEnabled: Bool, canBuyInBeta: Bool, openAppStorePage: @escaping () -> Void) -> ViewController { public func sgPayWallController(statusSignal: Signal<Int64, NoError>, replacementController: ViewController, presentationData: PresentationData? = nil, SGIAPManager: SGIAPManager, openUrl: @escaping (String, Bool) -> Void /* url, forceExternal */, paymentsEnabled: Bool, canBuyInBeta: Bool, openAppStorePage: @escaping () -> Void, proSupportUrl: String?) -> ViewController {
// let theme = presentationData?.theme ?? (UITraitCollection.current.userInterfaceStyle == .dark ? defaultDarkColorPresentationTheme : defaultPresentationTheme) // let theme = presentationData?.theme ?? (UITraitCollection.current.userInterfaceStyle == .dark ? defaultDarkColorPresentationTheme : defaultPresentationTheme)
let theme = defaultDarkColorPresentationTheme let theme = defaultDarkColorPresentationTheme
let strings = presentationData?.strings ?? defaultPresentationStrings let strings = presentationData?.strings ?? defaultPresentationStrings
@ -26,11 +26,12 @@ public func sgPayWallController(statusSignal: Signal<Int64, NoError>, replacemen
// legacyController.displayNavigationBar = false // legacyController.displayNavigationBar = false
legacyController.statusBar.statusBarStyle = .White legacyController.statusBar.statusBarStyle = .White
legacyController.attemptNavigation = { _ in return false } legacyController.attemptNavigation = { _ in return false }
legacyController.view.disablesInteractiveTransitionGestureRecognizer = true
let swiftUIView = SGSwiftUIView<SGPayWallView>( let swiftUIView = SGSwiftUIView<SGPayWallView>(
legacyController: legacyController, legacyController: legacyController,
content: { content: {
SGPayWallView(wrapperController: legacyController, replacementController: replacementController, SGIAP: SGIAPManager, statusSignal: statusSignal, openUrl: openUrl, openAppStorePage: openAppStorePage, paymentsEnabled: paymentsEnabled, canBuyInBeta: canBuyInBeta) SGPayWallView(wrapperController: legacyController, replacementController: replacementController, SGIAP: SGIAPManager, statusSignal: statusSignal, openUrl: openUrl, openAppStorePage: openAppStorePage, paymentsEnabled: paymentsEnabled, canBuyInBeta: canBuyInBeta, proSupportUrl: proSupportUrl)
} }
) )
let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true) let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true)
@ -95,6 +96,226 @@ struct BackgroundView: View {
} }
@available(iOS 13.0, *)
struct SGPayWallFeatureDetails: View {
let dismissAction: () -> Void
var bottomOffset: CGFloat = 0.0
let contentHeight: CGFloat = 690.0
let features: [SGProFeature]
@State var shownFeature: SGProFeatureId?
// Add animation states
@State private var showBackground = false
@State private var showContent = false
@State private var dragOffset: CGFloat = 0
var body: some View {
ZStack(alignment: .bottom) {
// Background overlay
if showBackground {
Color.black.opacity(0.4)
.zIndex(0)
.edgesIgnoringSafeArea(.all)
.onTapGesture {
dismissWithAnimation()
}
.transition(.opacity)
}
// Bottom sheet content
if showContent {
VStack {
if #available(iOS 14.0, *) {
TabView(selection: $shownFeature) {
ForEach(features) { feature in
ScrollView(showsIndicators: false) {
SGProFeatureView(
feature: feature
)
Color.clear.frame(height: 8.0) // paginator padding
}
.tag(feature.id)
.scrollBounceBehaviorIfAvailable(.basedOnSize)
}
}
.tabViewStyle(.page)
.padding(.bottom, bottomOffset - 8.0)
}
// Spacer for purchase buttons
if !bottomOffset.isZero {
Color.clear.frame(height: bottomOffset)
}
}
.zIndex(1)
.frame(maxHeight: contentHeight)
.background(Color(.black))
.cornerRadius(8, corners: [.topLeft, .topRight])
.overlay(closeButtonView)
.offset(y: max(0, dragOffset))
.gesture(
DragGesture()
.onChanged { value in
// Only track downward movement
if value.translation.height > 0 {
dragOffset = value.translation.height
}
}
.onEnded { value in
// If dragged down more than 150 points or with significant velocity, dismiss
if value.translation.height > 150 || value.predictedEndTranslation.height > 200 {
dismissWithAnimation()
} else {
// Otherwise, reset position
withAnimation(.spring()) {
dragOffset = 0
}
}
}
)
.transition(.move(edge: .bottom))
}
}
.onAppear {
appearWithAnimation()
}
}
private func appearWithAnimation() {
withAnimation(.easeIn(duration: 0.2)) {
showBackground = true
}
withAnimation(.spring(duration: 0.3)/*.delay(0.1)*/) {
showContent = true
}
}
private func dismissWithAnimation() {
withAnimation(.spring()) {
showContent = false
dragOffset = 0
}
withAnimation(.easeOut(duration: 0.2).delay(0.1)) {
showBackground = false
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
dismissAction()
}
}
private var closeButtonView: some View {
Button(action: {
dismissWithAnimation()
}) {
Image(systemName: "xmark")
.font(.headline)
.foregroundColor(.secondary.opacity(0.6))
.frame(width: 44, height: 44)
.contentShape(Rectangle()) // Improve tappable area
}
.opacity(showContent ? 1.0 : 0.0)
.padding([.top, .trailing], 8)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
}
}
@available(iOS 13.0, *)
struct SGProFeatureView: View {
let feature: SGProFeature
var body: some View {
VStack(spacing: 16) {
feature.image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity, maxHeight: 400.0, alignment: .top)
.clipped()
VStack(alignment: .center, spacing: 8) {
Text(feature.title)
.font(.title)
.fontWeight(.bold)
.multilineTextAlignment(.center)
Text(featureSubtitle)
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
.padding(.horizontal)
Spacer()
}
}
var featureSubtitle: String {
return feature.description ?? feature.subtitle
}
}
enum SGProFeatureId: Hashable {
case backup
case filter
case notifications
case toolbar
case icons
}
@available(iOS 13.0, *)
struct SGProFeature: Identifiable {
let id: SGProFeatureId
let title: String
let subtitle: String
let description: String?
@ViewBuilder
public var icon: some View {
switch (id) {
case .backup:
FeatureIcon(icon: "lock.fill", backgroundColor: .blue)
case .filter:
FeatureIcon(icon: "nosign", backgroundColor: .gray, fontWeight: .bold)
case .notifications:
FeatureIcon(icon: "bell.badge.slash.fill", backgroundColor: .red)
case .toolbar:
FeatureIcon(icon: "bold.underline", backgroundColor: .blue, iconSize: 16)
case .icons:
Image("SwiftgramSettings")
.resizable()
.frame(width: 32, height: 32)
@unknown default:
Image("SwiftgramPro")
.resizable()
.frame(width: 32, height: 32)
}
}
public var image: Image {
switch (id) {
case .backup:
return Image("ProDetailsBackup")
case .filter:
return Image("ProDetailsFilter")
case .notifications:
return Image("ProDetailsMute")
case .toolbar:
return Image("ProDetailsFormatting")
case .icons:
return Image("ProDetailsIcons")
@unknown default:
return Image("pro")
}
}
}
@available(iOS 13.0, *) @available(iOS 13.0, *)
struct SGPayWallView: View { struct SGPayWallView: View {
@Environment(\.navigationBarHeight) var navigationBarHeight: CGFloat @Environment(\.navigationBarHeight) var navigationBarHeight: CGFloat
@ -105,10 +326,11 @@ struct SGPayWallView: View {
let replacementController: ViewController let replacementController: ViewController
let SGIAP: SGIAPManager let SGIAP: SGIAPManager
let statusSignal: Signal<Int64, NoError> let statusSignal: Signal<Int64, NoError>
let openUrl: (String) -> Void let openUrl: (String, Bool) -> Void // url, forceExternal
let openAppStorePage: () -> Void let openAppStorePage: () -> Void
let paymentsEnabled: Bool let paymentsEnabled: Bool
let canBuyInBeta: Bool let canBuyInBeta: Bool
let proSupportUrl: String?
private enum PayWallState: Equatable { private enum PayWallState: Equatable {
case ready // ready to buy case ready // ready to buy
@ -123,6 +345,8 @@ struct SGPayWallView: View {
@State private var state: PayWallState = .ready @State private var state: PayWallState = .ready
@State private var showErrorAlert: Bool = false @State private var showErrorAlert: Bool = false
@State private var showConfetti: Bool = false @State private var showConfetti: Bool = false
@State private var showDetails: Bool = false
@State private var shownFeature: SGProFeatureId? = nil
private let productsPub = NotificationCenter.default.publisher(for: .SGIAPHelperProductsUpdatedNotification, object: nil) private let productsPub = NotificationCenter.default.publisher(for: .SGIAPHelperProductsUpdatedNotification, object: nil)
private let buyOrRestoreSuccessPub = NotificationCenter.default.publisher(for: .SGIAPHelperPurchaseNotification, object: nil) private let buyOrRestoreSuccessPub = NotificationCenter.default.publisher(for: .SGIAPHelperPurchaseNotification, object: nil)
@ -134,6 +358,18 @@ struct SGPayWallView: View {
@State private var hapticFeedback: HapticFeedback? @State private var hapticFeedback: HapticFeedback?
private let confettiDuration: Double = 5.0 private let confettiDuration: Double = 5.0
@State private var purchaseSectionSize: CGSize = .zero
private var features: [SGProFeature] {
return [
SGProFeature(id: .backup, title: "PayWall.SessionBackup.Title".i18n(lang), subtitle: "PayWall.SessionBackup.Notice".i18n(lang), description: "PayWall.SessionBackup.Description".i18n(lang)),
SGProFeature(id: .filter, title: "PayWall.MessageFilter.Title".i18n(lang), subtitle: "PayWall.MessageFilter.Notice".i18n(lang), description: "PayWall.MessageFilter.Description".i18n(lang)),
SGProFeature(id: .notifications, title: "PayWall.Notifications.Title".i18n(lang), subtitle: "PayWall.Notifications.Notice".i18n(lang), description: "PayWall.Notifications.Description".i18n(lang)),
SGProFeature(id: .toolbar, title: "PayWall.InputToolbar.Title".i18n(lang), subtitle: "PayWall.InputToolbar.Notice".i18n(lang), description: "PayWall.InputToolbar.Description".i18n(lang)),
SGProFeature(id: .icons, title: "PayWall.AppIcons.Title".i18n(lang), subtitle: "PayWall.AppIcons.Notice".i18n(lang), description: nil)
]
}
var body: some View { var body: some View {
ZStack { ZStack {
BackgroundView() BackgroundView()
@ -178,15 +414,24 @@ struct SGPayWallView: View {
// Spacer for purchase buttons // Spacer for purchase buttons
Color.clear.frame(height: 50) Color.clear.frame(height: (purchaseSectionSize.height / 2.0))
} }
.padding(.vertical, 50) .padding(.vertical, (purchaseSectionSize.height / 2.0))
} }
.padding(.leading, max(innerShadowWidth + 8.0, sgLeftSafeAreaInset(containerViewLayout))) .padding(.leading, max(innerShadowWidth + 8.0, sgLeftSafeAreaInset(containerViewLayout)))
.padding(.trailing, max(innerShadowWidth + 8.0, sgRightSafeAreaInset(containerViewLayout))) .padding(.trailing, max(innerShadowWidth + 8.0, sgRightSafeAreaInset(containerViewLayout)))
if showDetails {
SGPayWallFeatureDetails(
dismissAction: dismissDetails,
bottomOffset: (purchaseSectionSize.height / 2.0) * 0.9, // reduced offset for paginator
features: features,
shownFeature: shownFeature)
}
// Fixed purchase button at bottom // Fixed purchase button at bottom
purchaseSection purchaseSection
.trackSize($purchaseSectionSize)
} }
} }
.confetti(isActive: $showConfetti, duration: confettiDuration) .confetti(isActive: $showConfetti, duration: confettiDuration)
@ -248,37 +493,16 @@ struct SGPayWallView: View {
private var featuresSection: some View { private var featuresSection: some View {
VStack(spacing: 8) { VStack(spacing: 8) {
FeatureRow( ForEach(features) { feature in
icon: FeatureIcon(icon: "lock.fill", backgroundColor: .blue), FeatureRow(
title: "PayWall.SessionBackup.Title".i18n(lang), icon: feature.icon,
subtitle: "PayWall.SessionBackup.Notice".i18n(lang) title: feature.title,
) subtitle: feature.subtitle,
action: {
FeatureRow( showDetailsForFeature(feature.id)
icon: FeatureIcon(icon: "nosign", backgroundColor: .gray, fontWeight: .bold), }
title: "PayWall.MessageFilter.Title".i18n(lang), )
subtitle: "PayWall.MessageFilter.Notice".i18n(lang) }
)
FeatureRow(
icon: FeatureIcon(icon: "bell.badge.slash.fill", backgroundColor: .red),
title: "PayWall.Notifications.Title".i18n(lang),
subtitle: "PayWall.Notifications.Notice".i18n(lang)
)
FeatureRow(
icon: FeatureIcon(icon: "bold.underline", backgroundColor: .blue, iconSize: 16),
title: "PayWall.InputToolbar.Title".i18n(lang),
subtitle: "PayWall.InputToolbar.Notice".i18n(lang)
)
FeatureRow(
icon: Image("SwiftgramSettings")
.resizable()
.frame(width: 32, height: 32),
title: "PayWall.AppIcons.Title".i18n(lang),
subtitle: "PayWall.AppIcons.Notice".i18n(lang)
)
} }
} }
@ -296,20 +520,36 @@ struct SGPayWallView: View {
private var purchaseSection: some View { private var purchaseSection: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
Divider() Divider()
VStack(spacing: 8) {
Button(action: handlePurchase) {
Text(buttonTitle)
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
.padding()
.background(Color(hex: accentColorHex))
.foregroundColor(.white)
.cornerRadius(12)
}
.disabled((state != .ready || !canPurchase) && !(currentStatus > 1))
.opacity(((state != .ready || !canPurchase) && !(currentStatus > 1)) ? 0.5 : 1.0)
Button(action: handlePurchase) { if let proSupportUrl = proSupportUrl {
Text(buttonTitle) HStack(alignment: .center, spacing: 4) {
.fontWeight(.semibold) Text("PayWall.ProSupport.Title".i18n(lang))
.frame(maxWidth: .infinity) .font(.caption)
.padding() .foregroundColor(.secondary)
.background(Color(hex: accentColorHex)) Button(action: {
.foregroundColor(.white) openUrl(proSupportUrl, false)
.cornerRadius(12) }) {
Text("PayWall.ProSupport.Contact".i18n(lang))
.font(.caption)
.foregroundColor(Color(hex: accentColorHex))
}
}
}
} }
.disabled((state != .ready || !canPurchase) && !(currentStatus > 1))
.opacity(((state != .ready || !canPurchase) && !(currentStatus > 1)) ? 0.5 : 1.0)
.padding([.horizontal, .top]) .padding([.horizontal, .top])
.padding(.bottom, sgBottomSafeAreaInset(containerViewLayout)) .padding(.bottom, sgBottomSafeAreaInset(containerViewLayout) + 2.0)
} }
.foregroundColor(Color.black) .foregroundColor(Color.black)
.backgroundIfAvailable(material: .ultraThinMaterial) .backgroundIfAvailable(material: .ultraThinMaterial)
@ -324,7 +564,7 @@ struct SGPayWallView: View {
.tint(Color(hex: accentColorHex)) .tint(Color(hex: accentColorHex))
.foregroundColor(.secondary) .foregroundColor(.secondary)
.environment(\.openURL, OpenURLAction { url in .environment(\.openURL, OpenURLAction { url in
openUrl(url.absoluteString) openUrl(url.absoluteString, false)
return .handled return .handled
}) })
} else { } else {
@ -333,14 +573,14 @@ struct SGPayWallView: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
HStack(alignment: .top, spacing: 8) { HStack(alignment: .top, spacing: 8) {
Button(action: { Button(action: {
openUrl("PayWall.PrivacyURL".i18n(lang)) openUrl("PayWall.PrivacyURL".i18n(lang), true)
}) { }) {
Text("PayWall.Privacy".i18n(lang)) Text("PayWall.Privacy".i18n(lang))
.font(.caption) .font(.caption)
.foregroundColor(Color(hex: accentColorHex)) .foregroundColor(Color(hex: accentColorHex))
} }
Button(action: { Button(action: {
openUrl("PayWall.TermsURL".i18n(lang)) openUrl("PayWall.TermsURL".i18n(lang), true)
}) { }) {
Text("PayWall.Terms".i18n(lang)) Text("PayWall.Terms".i18n(lang))
.font(.caption) .font(.caption)
@ -369,7 +609,7 @@ struct SGPayWallView: View {
} }
HStack { HStack {
Button(action: { Button(action: {
openUrl("PayWall.About.SignatureURL".i18n(lang)) openUrl("PayWall.About.SignatureURL".i18n(lang), false)
}) { }) {
Text("PayWall.About.Signature".i18n(lang)) Text("PayWall.About.Signature".i18n(lang))
.font(.caption) .font(.caption)
@ -390,6 +630,8 @@ struct SGPayWallView: View {
.frame(width: 44, height: 44) .frame(width: 44, height: 44)
.contentShape(Rectangle()) // Improve tappable area .contentShape(Rectangle()) // Improve tappable area
} }
.disabled(showDetails)
.opacity(showDetails ? 0.0 : 1.0)
.padding([.top, .trailing], 16) .padding([.top, .trailing], 16)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
} }
@ -426,6 +668,18 @@ struct SGPayWallView: View {
} }
} }
private func showDetailsForFeature(_ featureId: SGProFeatureId) {
if #available(iOS 14.0, *) {
shownFeature = featureId
showDetails = true
} // pagination is not available on iOS 13
}
private func dismissDetails() {
// shownFeature = nil
showDetails = false
}
private func updateSelectedProduct() { private func updateSelectedProduct() {
product = SGIAP.availableProducts.first { $0.id == SG_CONFIG.iaps.first ?? "" } product = SGIAP.availableProducts.first { $0.id == SG_CONFIG.iaps.first ?? "" }
} }
@ -515,11 +769,10 @@ struct FeatureRow<IconContent: View>: View {
let icon: IconContent let icon: IconContent
let title: String let title: String
let subtitle: String let subtitle: String
let action: () -> Void
var body: some View { var body: some View {
Button(action: { Button(action: action) {
// TODO(swiftgram): Feature row clarification
}) {
HStack(spacing: 16) { HStack(spacing: 16) {
HStack(alignment: .top, spacing: 12) { HStack(alignment: .top, spacing: 12) {
@ -537,10 +790,11 @@ struct FeatureRow<IconContent: View>: View {
} }
Spacer() Spacer()
// TODO(swiftgram): uncomment if #available(iOS 14.0, *) {
// Image(systemName: "chevron.right") Image(systemName: "chevron.right")
// .font(.system(size: 12, weight: .semibold)) .font(.system(size: 12, weight: .semibold))
// .foregroundColor(.secondary) .foregroundColor(.secondary)
} // Descriptions are not available on iOS 13
} }
.padding() .padding()
.background( .background(

View File

@ -195,25 +195,33 @@
"PayWall.SessionBackup.Title" = "Accounts Backup"; "PayWall.SessionBackup.Title" = "Accounts Backup";
"PayWall.SessionBackup.Notice" = "Log-in to accounts without code, even after reinstall. Secure storage with on-device Keychain."; "PayWall.SessionBackup.Notice" = "Log-in to accounts without code, even after reinstall. Secure storage with on-device Keychain.";
"PayWall.SessionBackup.Description" = "Changing device or deleting Swiftgram is no longer an issue. Restore all Sessions that are still Active on Telegram servers.";
"PayWall.MessageFilter.Title" = "Message Filter"; "PayWall.MessageFilter.Title" = "Message Filter";
"PayWall.MessageFilter.Notice" = "Reduce visibility of SPAM, promotions and annoying messages."; "PayWall.MessageFilter.Notice" = "Reduce visibility of SPAM, promotions and annoying messages.";
"PayWall.MessageFilter.Description" = "Create a list of keywords you don't want to see often and Swiftgram will reduce distractions.";
"PayWall.Notifications.Title" = "Disable @mentions and replies"; "PayWall.Notifications.Title" = "Disable @mentions and replies";
"PayWall.Notifications.Notice" = "Hide or mute non-important notifications."; "PayWall.Notifications.Notice" = "Hide or mute non-important notifications.";
"PayWall.Notifications.Description" = "No more Pinned Messages or @mentions when you need some piece of mind.";
"PayWall.InputToolbar.Title" = "Formatting Panel"; "PayWall.InputToolbar.Title" = "Formatting Panel";
"PayWall.InputToolbar.Notice" = "Save time formatting messages with just a single tap."; "PayWall.InputToolbar.Notice" = "Save time formatting messages with just a single tap.";
"PayWall.InputToolbar.Description" = "Apply and clear Formatting or insert new lines like a Pro.";
"PayWall.AppIcons.Title" = "Unique App Icons"; "PayWall.AppIcons.Title" = "Unique App Icons";
"PayWall.AppIcons.Notice" = "Customize Swiftgram look on your home screen."; "PayWall.AppIcons.Notice" = "Customize Swiftgram look on your home screen.";
"PayWall.About.Title" = "About Swiftgram Pro"; "PayWall.About.Title" = "About Swiftgram Pro";
"PayWall.About.Notice" = "Free version of Swiftgram provides dozens of features and improvements over Telegram app. Innovating and keeping Swiftgram in sync with monthly Telegram updates is a huge effort that requires a lot of time and expensive hardware.\n\nSwiftgram is an open-source app that respects your privacy and doesn't bother you with ads. Subscribing to Swiftgram Pro you get access to exclusive features and support an independent developer."; "PayWall.About.Notice" = "Free version of Swiftgram provides dozens of features and improvements over Telegram app. Innovating and keeping Swiftgram in sync with monthly Telegram updates is a huge effort that requires a lot of time and expensive hardware.\n\nSwiftgram is an open-source app that respects your privacy and doesn't bother you with ads. Subscribing to Swiftgram Pro you get access to exclusive features and support an independent developer.";
/* DO NOT TRANSLATE */
"PayWall.About.Signature" = "@Kylmakalle"; "PayWall.About.Signature" = "@Kylmakalle";
/* DO NOT TRANSLATE */ /* DO NOT TRANSLATE */
"PayWall.About.SignatureURL" = "https://t.me/Kylmakalle"; "PayWall.About.SignatureURL" = "https://t.me/Kylmakalle";
"PayWall.ProSupport.Title" = "Troubles with payment?";
"PayWall.ProSupport.Contact" = "No worries!";
"PayWall.RestorePurchases" = "Restore Purchases"; "PayWall.RestorePurchases" = "Restore Purchases";
"PayWall.Terms" = "Terms of Service"; "PayWall.Terms" = "Terms of Service";
"PayWall.Privacy" = "Privacy Policy"; "PayWall.Privacy" = "Privacy Policy";

View File

@ -413,6 +413,21 @@ public enum BackgroundMaterial {
} }
} }
public enum BounceBehavior {
case automatic
case always
case basedOnSize
@available(iOS 16.4, *)
var behavior: ScrollBounceBehavior {
switch self {
case .automatic: return .automatic
case .always: return .always
case .basedOnSize: return .basedOnSize
}
}
}
@available(iOS 13.0, *) @available(iOS 13.0, *)
public extension View { public extension View {
@ -437,3 +452,62 @@ public extension View {
} }
} }
} }
@available(iOS 13.0, *)
public extension View {
func scrollBounceBehaviorIfAvailable(_ behavior: BounceBehavior) -> some View {
if #available(iOS 16.4, *) {
return self.scrollBounceBehavior(behavior.behavior)
} else {
return self
}
}
}
@available(iOS 13.0, *)
public extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape(RoundedCorner(radius: radius, corners: corners))
}
}
@available(iOS 13.0, *)
public struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
public func path(in rect: CGRect) -> Path {
let path = UIBezierPath(
roundedRect: rect,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius)
)
return Path(path.cgPath)
}
}
@available(iOS 13.0, *)
public struct ContentSizeModifier: ViewModifier {
@Binding var size: CGSize
public func body(content: Content) -> some View {
content
.background(
GeometryReader { geometry -> Color in
if geometry.size != size {
DispatchQueue.main.async {
self.size = geometry.size
}
}
return Color.clear
}
)
}
}
@available(iOS 13.0, *)
public extension View {
func trackSize(_ size: Binding<CGSize>) -> some View {
self.modifier(ContentSizeModifier(size: size))
}
}

View File

@ -5,7 +5,7 @@ public struct SGWebSettings: Codable, Equatable {
public let user: SGUserSettings public let user: SGUserSettings
public static var defaultValue: SGWebSettings { public static var defaultValue: SGWebSettings {
return SGWebSettings(global: SGGlobalSettings(ytPip: true, qrLogin: true, storiesAvailable: false, canViewMessages: true, canEditSettings: false, canShowTelescope: false, announcementsData: nil, regdateFormat: "month", botMonkeys: [], forceReasons: [], unforceReasons: [], paymentsEnabled: true, duckyAppIconAvailable: true, canGrant: false), user: SGUserSettings(contentReasons: [], canSendTelescope: false, canBuyInBeta: true)) return SGWebSettings(global: SGGlobalSettings(ytPip: true, qrLogin: true, storiesAvailable: false, canViewMessages: true, canEditSettings: false, canShowTelescope: false, announcementsData: nil, regdateFormat: "month", botMonkeys: [], forceReasons: [], unforceReasons: [], paymentsEnabled: true, duckyAppIconAvailable: true, canGrant: false, proSupportUrl: nil), user: SGUserSettings(contentReasons: [], canSendTelescope: false, canBuyInBeta: true))
} }
} }
@ -24,6 +24,7 @@ public struct SGGlobalSettings: Codable, Equatable {
public let paymentsEnabled: Bool public let paymentsEnabled: Bool
public let duckyAppIconAvailable: Bool public let duckyAppIconAvailable: Bool
public let canGrant: Bool public let canGrant: Bool
public let proSupportUrl: String?
} }
public struct SGBotMonkeys: Codable, Equatable { public struct SGBotMonkeys: Codable, Equatable {

View File

@ -4,6 +4,8 @@ import SGConfig
import SGSettingsUI import SGSettingsUI
import SGDebugUI import SGDebugUI
import SFSafariViewControllerPlus import SFSafariViewControllerPlus
import UndoUI
//
import ContactListUI import ContactListUI
import Foundation import Foundation
import Display import Display
@ -1041,6 +1043,33 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
} }
} }
} }
case "restart":
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let lang = presentationData.strings.baseLanguageCode
context.sharedContext.presentGlobalController(
UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil,
text: "Common.RestartRequired".i18n(lang),
timeout: nil,
customUndoText: "Common.RestartNow".i18n(lang)
),
elevatedLayout: false,
action: { action in if action == .undo { exit(0) }; return true }
),
nil
)
case "restore_purchases", "pro_restore", "validate", "restore":
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let lang = presentationData.strings.baseLanguageCode
context.sharedContext.presentGlobalController(UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil, text: "PayWall.Button.Restoring".i18n(lang), timeout: nil, customUndoText: nil),
elevatedLayout: false,
action: { _ in return false }
),
nil)
context.sharedContext.SGIAP?.restorePurchases {}
default: default:
break break
} }

View File

@ -3625,7 +3625,23 @@ extension SharedAccountContextImpl {
let proController = self.makeSGProController(context: context) let proController = self.makeSGProController(context: context)
let sgWebSettings = context.currentAppConfiguration.with { $0 }.sgWebSettings let sgWebSettings = context.currentAppConfiguration.with { $0 }.sgWebSettings
let payWallController = sgPayWallController(statusSignal: statusSignal, replacementController: proController, presentationData: self.currentPresentationData.with { $0 }, SGIAPManager: sgIAP, openUrl: self.applicationBindings.openUrl, paymentsEnabled: sgWebSettings.global.paymentsEnabled, canBuyInBeta: sgWebSettings.user.canBuyInBeta, openAppStorePage: self.applicationBindings.openAppStorePage) let presentationData = self.currentPresentationData.with { $0 }
var payWallController: ViewController? = nil
let openUrl: ((String, Bool) -> Void) = { [weak self, weak context] url, forceExternal in
guard let strongSelf = self, let strongContext = context, let strongPayWallController = payWallController else {
return
}
let navigationController = strongPayWallController.navigationController as? NavigationController
Queue.mainQueue().async {
strongSelf.openExternalUrl(context: strongContext, urlContext: .generic, url: url, forceExternal: forceExternal, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
}
}
var supportUrl: String? = nil
if let supportUrlString = sgWebSettings.global.proSupportUrl, !supportUrlString.isEmpty, let data = Data(base64Encoded: supportUrlString), let decodedString = String(data: data, encoding: .utf8) {
supportUrl = decodedString
}
payWallController = sgPayWallController(statusSignal: statusSignal, replacementController: proController, presentationData: presentationData, SGIAPManager: sgIAP, openUrl: openUrl, paymentsEnabled: sgWebSettings.global.paymentsEnabled, canBuyInBeta: sgWebSettings.user.canBuyInBeta, openAppStorePage: self.applicationBindings.openAppStorePage, proSupportUrl: supportUrl)
return payWallController return payWallController
} }