mirror of
https://github.com/h3pdesign/Neon-Vision-Editor
synced 2026-04-21 13:27:16 +00:00
Fix support tip IAP in TestFlight
This commit is contained in:
parent
960e1e8466
commit
da9a4ce68b
10 changed files with 239 additions and 93 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -4,6 +4,7 @@
|
|||
# Xcode
|
||||
DerivedData/
|
||||
.DerivedData/
|
||||
.DerivedData*/
|
||||
DerivedData-*/
|
||||
*.xcresult
|
||||
*.xcuserstate
|
||||
|
|
|
|||
|
|
@ -361,11 +361,10 @@
|
|||
CODE_SIGNING_ALLOWED = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 334;
|
||||
CURRENT_PROJECT_VERSION = 341;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = CS727NF72U;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
|
|
@ -442,11 +441,10 @@
|
|||
CODE_SIGNING_ALLOWED = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 334;
|
||||
CURRENT_PROJECT_VERSION = 341;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = CS727NF72U;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import StoreKit
|
|||
// Handles optional consumable support purchase state via StoreKit.
|
||||
@MainActor
|
||||
final class SupportPurchaseManager: ObservableObject {
|
||||
static let supportProductID = "h3p.neon-vision-editor.support.optional"
|
||||
static let supportProductID = "002420160"
|
||||
|
||||
@Published private(set) var supportProduct: Product?
|
||||
@Published private(set) var hasSupported: Bool = false
|
||||
|
|
@ -77,7 +77,15 @@ final class SupportPurchaseManager: ObservableObject {
|
|||
supportProduct = nil
|
||||
isLoadingProducts = false
|
||||
if showStatusOnFailure {
|
||||
#if os(iOS)
|
||||
if !AppStore.canMakePayments {
|
||||
statusMessage = NSLocalizedString("In-App Purchases are disabled on this device. Check App Store login and Screen Time restrictions.", comment: "")
|
||||
} else {
|
||||
statusMessage = NSLocalizedString("App Store pricing is only available in App Store/TestFlight builds.", comment: "")
|
||||
}
|
||||
#else
|
||||
statusMessage = NSLocalizedString("App Store pricing is only available in App Store/TestFlight builds.", comment: "")
|
||||
#endif
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -91,7 +99,11 @@ final class SupportPurchaseManager: ObservableObject {
|
|||
statusMessage = nil
|
||||
}
|
||||
if supportProduct == nil, showStatusOnFailure {
|
||||
statusMessage = NSLocalizedString("Could not load App Store product. Please try again.", comment: "")
|
||||
let format = NSLocalizedString(
|
||||
"App Store did not return product %@. Check App Store Connect and TestFlight availability.",
|
||||
comment: ""
|
||||
)
|
||||
statusMessage = String(format: format, Self.supportProductID)
|
||||
}
|
||||
} catch {
|
||||
if showStatusOnFailure {
|
||||
|
|
@ -111,7 +123,15 @@ final class SupportPurchaseManager: ObservableObject {
|
|||
// Starts purchase flow for the optional support product.
|
||||
func purchaseSupport() async {
|
||||
guard canUseInAppPurchases else {
|
||||
#if os(iOS)
|
||||
if !AppStore.canMakePayments {
|
||||
statusMessage = NSLocalizedString("In-App Purchases are disabled on this device. Check App Store login and Screen Time restrictions.", comment: "")
|
||||
} else {
|
||||
statusMessage = NSLocalizedString("In-app purchase is only available in App Store/TestFlight builds.", comment: "")
|
||||
}
|
||||
#else
|
||||
statusMessage = NSLocalizedString("In-app purchase is only available in App Store/TestFlight builds.", comment: "")
|
||||
#endif
|
||||
return
|
||||
}
|
||||
if supportProduct == nil {
|
||||
|
|
@ -141,27 +161,63 @@ final class SupportPurchaseManager: ObservableObject {
|
|||
statusMessage = NSLocalizedString("Purchase did not complete.", comment: "")
|
||||
}
|
||||
} catch {
|
||||
let format = NSLocalizedString("Purchase failed: %@", comment: "")
|
||||
statusMessage = String(format: format, error.localizedDescription)
|
||||
let details = String(describing: error)
|
||||
if details == error.localizedDescription {
|
||||
let format = NSLocalizedString("Purchase failed: %@", comment: "")
|
||||
statusMessage = String(format: format, error.localizedDescription)
|
||||
} else {
|
||||
let format = NSLocalizedString("Purchase failed: %@ (%@)", comment: "")
|
||||
statusMessage = String(format: format, error.localizedDescription, details)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detects whether this build/environment can use in-app purchases.
|
||||
private func refreshBypassEligibility() async {
|
||||
#if os(iOS)
|
||||
canUseInAppPurchases = AppStore.canMakePayments
|
||||
#else
|
||||
canUseInAppPurchases = false
|
||||
#endif
|
||||
do {
|
||||
let appTransactionResult = try await AppTransaction.shared
|
||||
switch appTransactionResult {
|
||||
case .verified(let appTransaction):
|
||||
canUseInAppPurchases = true
|
||||
#if os(iOS)
|
||||
switch appTransaction.environment {
|
||||
case .production, .sandbox:
|
||||
canUseInAppPurchases = AppStore.canMakePayments
|
||||
case .xcode:
|
||||
#if targetEnvironment(simulator) || DEBUG
|
||||
canUseInAppPurchases = AppStore.canMakePayments
|
||||
#else
|
||||
canUseInAppPurchases = false
|
||||
#endif
|
||||
default:
|
||||
canUseInAppPurchases = AppStore.canMakePayments
|
||||
}
|
||||
#else
|
||||
canUseInAppPurchases = false
|
||||
#endif
|
||||
allowsTestingBypass = shouldAllowTestingBypass(environment: appTransaction.environment)
|
||||
case .unverified:
|
||||
canUseInAppPurchases = false
|
||||
canUseInAppPurchases = AppStore.canMakePayments
|
||||
allowsTestingBypass = false
|
||||
}
|
||||
} catch {
|
||||
#if os(iOS)
|
||||
canUseInAppPurchases = AppStore.canMakePayments
|
||||
#else
|
||||
canUseInAppPurchases = false
|
||||
#endif
|
||||
allowsTestingBypass = false
|
||||
}
|
||||
|
||||
#if targetEnvironment(simulator) || DEBUG
|
||||
if !allowsTestingBypass {
|
||||
allowsTestingBypass = true
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Listens for transaction updates and applies verified changes.
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
"locale" : "de_DE"
|
||||
}
|
||||
],
|
||||
"productID" : "h3p.neon-vision-editor.support.optional",
|
||||
"productID" : "002420160",
|
||||
"referenceName" : "Optional Support Tip",
|
||||
"type" : "Consumable"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2782,6 +2782,7 @@ struct ContentView: View {
|
|||
// Single editor (no TabView)
|
||||
CustomTextEditor(
|
||||
text: currentContentBinding,
|
||||
documentID: viewModel.selectedTabID,
|
||||
language: currentLanguage,
|
||||
colorScheme: colorScheme,
|
||||
fontSize: editorFontSize,
|
||||
|
|
|
|||
|
|
@ -1612,6 +1612,7 @@ final class AcceptingTextView: NSTextView {
|
|||
// NSViewRepresentable wrapper around NSTextView to integrate with SwiftUI.
|
||||
struct CustomTextEditor: NSViewRepresentable {
|
||||
@Binding var text: String
|
||||
let documentID: UUID?
|
||||
let language: String
|
||||
let colorScheme: ColorScheme
|
||||
let fontSize: CGFloat
|
||||
|
|
@ -1879,21 +1880,32 @@ struct CustomTextEditor: NSViewRepresentable {
|
|||
// Keep NSTextView in sync with SwiftUI state and schedule highlighting when needed.
|
||||
func updateNSView(_ nsView: NSScrollView, context: Context) {
|
||||
if let textView = nsView.documentView as? NSTextView {
|
||||
var needsLayoutRefresh = false
|
||||
var didChangeRulerConfiguration = false
|
||||
textView.isEditable = true
|
||||
textView.isSelectable = true
|
||||
let acceptingView = textView as? AcceptingTextView
|
||||
let isDropApplyInFlight = acceptingView?.isApplyingDroppedContent ?? false
|
||||
let didSwitchDocument = context.coordinator.lastDocumentID != documentID
|
||||
let didFinishTabLoad = (context.coordinator.lastTabLoadingContent == true) && !isTabLoadingContent
|
||||
if didSwitchDocument {
|
||||
context.coordinator.lastDocumentID = documentID
|
||||
context.coordinator.cancelPendingBindingSync()
|
||||
context.coordinator.invalidateHighlightCache()
|
||||
}
|
||||
context.coordinator.lastTabLoadingContent = isTabLoadingContent
|
||||
|
||||
// Sanitize and avoid publishing binding during update
|
||||
let target = sanitizedForExternalSet(text)
|
||||
if textView.string != target {
|
||||
let hasFocus = (textView.window?.firstResponder as? NSTextView) === textView
|
||||
let shouldPreferEditorBuffer = hasFocus && !isTabLoadingContent
|
||||
let shouldPreferEditorBuffer = hasFocus && !isTabLoadingContent && !didSwitchDocument && !didFinishTabLoad
|
||||
if shouldPreferEditorBuffer {
|
||||
context.coordinator.syncBindingTextImmediately(textView.string)
|
||||
} else {
|
||||
context.coordinator.cancelPendingBindingSync()
|
||||
replaceTextPreservingSelectionAndFocus(textView, with: target)
|
||||
needsLayoutRefresh = true
|
||||
context.coordinator.invalidateHighlightCache()
|
||||
DispatchQueue.main.async {
|
||||
if self.text != target {
|
||||
|
|
@ -1906,19 +1918,23 @@ struct CustomTextEditor: NSViewRepresentable {
|
|||
let targetFont = resolvedFont()
|
||||
if textView.font != targetFont {
|
||||
textView.font = targetFont
|
||||
needsLayoutRefresh = true
|
||||
context.coordinator.invalidateHighlightCache()
|
||||
}
|
||||
if textView.textContainerInset.width != 6 || textView.textContainerInset.height != 8 {
|
||||
textView.textContainerInset = NSSize(width: 6, height: 8)
|
||||
needsLayoutRefresh = true
|
||||
}
|
||||
if textView.textContainer?.lineFragmentPadding != 4 {
|
||||
textView.textContainer?.lineFragmentPadding = 4
|
||||
needsLayoutRefresh = true
|
||||
}
|
||||
let style = paragraphStyle()
|
||||
let currentLineHeight = textView.defaultParagraphStyle?.lineHeightMultiple ?? 1.0
|
||||
if abs(currentLineHeight - style.lineHeightMultiple) > 0.0001 {
|
||||
textView.defaultParagraphStyle = style
|
||||
textView.typingAttributes[.paragraphStyle] = style
|
||||
needsLayoutRefresh = true
|
||||
let nsLen = (textView.string as NSString).length
|
||||
if nsLen <= 200_000, let storage = textView.textStorage {
|
||||
storage.beginEditing()
|
||||
|
|
@ -1933,6 +1949,7 @@ struct CustomTextEditor: NSViewRepresentable {
|
|||
let sanitized = AcceptingTextView.sanitizePlainText(textView.string)
|
||||
if sanitized != textView.string {
|
||||
replaceTextPreservingSelectionAndFocus(textView, with: sanitized)
|
||||
needsLayoutRefresh = true
|
||||
context.coordinator.invalidateHighlightCache()
|
||||
DispatchQueue.main.async {
|
||||
if self.text != sanitized {
|
||||
|
|
@ -1941,13 +1958,6 @@ struct CustomTextEditor: NSViewRepresentable {
|
|||
}
|
||||
}
|
||||
}
|
||||
if let storage = textView.textStorage {
|
||||
storage.beginEditing()
|
||||
let fullRange = NSRange(location: 0, length: storage.length)
|
||||
storage.removeAttribute(.underlineStyle, range: fullRange)
|
||||
storage.removeAttribute(.strikethroughStyle, range: fullRange)
|
||||
storage.endEditing()
|
||||
}
|
||||
|
||||
let theme = currentEditorTheme(colorScheme: colorScheme)
|
||||
|
||||
|
|
@ -1975,33 +1985,45 @@ struct CustomTextEditor: NSViewRepresentable {
|
|||
.backgroundColor: NSColor(theme.selection)
|
||||
]
|
||||
let showLineNumbersByDefault = showLineNumbers
|
||||
textView.usesRuler = showLineNumbersByDefault
|
||||
textView.isRulerVisible = showLineNumbersByDefault
|
||||
nsView.hasHorizontalRuler = false
|
||||
nsView.horizontalRulerView = nil
|
||||
nsView.hasVerticalRuler = showLineNumbersByDefault
|
||||
nsView.rulersVisible = showLineNumbersByDefault
|
||||
if textView.usesRuler != showLineNumbersByDefault {
|
||||
textView.usesRuler = showLineNumbersByDefault
|
||||
didChangeRulerConfiguration = true
|
||||
}
|
||||
if textView.isRulerVisible != showLineNumbersByDefault {
|
||||
textView.isRulerVisible = showLineNumbersByDefault
|
||||
didChangeRulerConfiguration = true
|
||||
}
|
||||
if nsView.hasHorizontalRuler {
|
||||
nsView.hasHorizontalRuler = false
|
||||
didChangeRulerConfiguration = true
|
||||
}
|
||||
if nsView.horizontalRulerView != nil {
|
||||
nsView.horizontalRulerView = nil
|
||||
didChangeRulerConfiguration = true
|
||||
}
|
||||
if nsView.hasVerticalRuler != showLineNumbersByDefault {
|
||||
nsView.hasVerticalRuler = showLineNumbersByDefault
|
||||
didChangeRulerConfiguration = true
|
||||
}
|
||||
if nsView.rulersVisible != showLineNumbersByDefault {
|
||||
nsView.rulersVisible = showLineNumbersByDefault
|
||||
didChangeRulerConfiguration = true
|
||||
}
|
||||
if showLineNumbersByDefault {
|
||||
if !(nsView.verticalRulerView is LineNumberRulerView) {
|
||||
nsView.verticalRulerView = LineNumberRulerView(textView: textView)
|
||||
didChangeRulerConfiguration = true
|
||||
}
|
||||
} else {
|
||||
nsView.verticalRulerView = nil
|
||||
}
|
||||
|
||||
// Defensive clear of underline/strikethrough styles (always clear)
|
||||
if let storage = textView.textStorage {
|
||||
storage.beginEditing()
|
||||
let fullRange = NSRange(location: 0, length: storage.length)
|
||||
storage.removeAttribute(.underlineStyle, range: fullRange)
|
||||
storage.removeAttribute(.strikethroughStyle, range: fullRange)
|
||||
storage.endEditing()
|
||||
if nsView.verticalRulerView != nil {
|
||||
nsView.verticalRulerView = nil
|
||||
didChangeRulerConfiguration = true
|
||||
}
|
||||
}
|
||||
|
||||
// Re-apply invisible-character visibility preference after style updates.
|
||||
applyInvisibleCharacterPreference(textView)
|
||||
|
||||
nsView.tile()
|
||||
// Keep the text container width in sync & relayout
|
||||
acceptingView?.autoIndentEnabled = autoIndentEnabled
|
||||
acceptingView?.autoCloseBracketsEnabled = autoCloseBracketsEnabled
|
||||
|
|
@ -2012,10 +2034,15 @@ struct CustomTextEditor: NSViewRepresentable {
|
|||
if context.coordinator.lastAppliedWrapMode != effectiveWrap {
|
||||
applyWrapMode(isWrapped: effectiveWrap, textView: textView, scrollView: nsView)
|
||||
context.coordinator.lastAppliedWrapMode = effectiveWrap
|
||||
needsLayoutRefresh = true
|
||||
}
|
||||
|
||||
textView.invalidateIntrinsicContentSize()
|
||||
nsView.reflectScrolledClipView(nsView.contentView)
|
||||
if didChangeRulerConfiguration {
|
||||
nsView.tile()
|
||||
}
|
||||
if needsLayoutRefresh {
|
||||
textView.invalidateIntrinsicContentSize()
|
||||
}
|
||||
|
||||
// Only schedule highlight if needed (e.g., language/color scheme changes or external text updates)
|
||||
context.coordinator.parent = self
|
||||
|
|
@ -2052,6 +2079,8 @@ struct CustomTextEditor: NSViewRepresentable {
|
|||
private var pendingEditedRange: NSRange?
|
||||
private var pendingBindingSync: DispatchWorkItem?
|
||||
var lastAppliedWrapMode: Bool?
|
||||
var lastDocumentID: UUID?
|
||||
var lastTabLoadingContent: Bool?
|
||||
var hasPendingBindingSync: Bool { pendingBindingSync != nil }
|
||||
|
||||
init(_ parent: CustomTextEditor) {
|
||||
|
|
@ -2190,6 +2219,18 @@ struct CustomTextEditor: NSViewRepresentable {
|
|||
lastTranslucencyEnabled == translucencyEnabled {
|
||||
return
|
||||
}
|
||||
let styleStateUnchanged = lang == lastLanguage &&
|
||||
scheme == lastColorScheme &&
|
||||
lastLineHeight == lineHeightValue &&
|
||||
lastHighlightToken == token &&
|
||||
lastTranslucencyEnabled == translucencyEnabled
|
||||
let selectionOnlyChange = text == lastHighlightedText &&
|
||||
styleStateUnchanged &&
|
||||
lastSelectionLocation != selectionLocation
|
||||
if selectionOnlyChange && parent.isLineWrapEnabled {
|
||||
lastSelectionLocation = selectionLocation
|
||||
return
|
||||
}
|
||||
let incrementalRange: NSRange? = {
|
||||
guard token == lastHighlightToken,
|
||||
lang == lastLanguage,
|
||||
|
|
@ -2320,7 +2361,6 @@ struct CustomTextEditor: NSViewRepresentable {
|
|||
guard generation == self.highlightGeneration else { return }
|
||||
// Discard if text changed since we started
|
||||
guard tv.string == textSnapshot else { return }
|
||||
let priorVisibleOrigin = tv.enclosingScrollView?.contentView.bounds.origin
|
||||
let baseColor = self.parent.effectiveBaseTextColor()
|
||||
self.isApplyingHighlight = true
|
||||
defer { self.isApplyingHighlight = false }
|
||||
|
|
@ -2390,15 +2430,6 @@ struct CustomTextEditor: NSViewRepresentable {
|
|||
|
||||
self.parent.applyInvisibleCharacterPreference(tv)
|
||||
|
||||
// Restore selection only if it hasn't changed since we started
|
||||
if NSEqualRanges(tv.selectedRange(), selected) {
|
||||
tv.setSelectedRange(selected)
|
||||
}
|
||||
if let clipView = tv.enclosingScrollView?.contentView, let priorVisibleOrigin {
|
||||
clipView.setBoundsOrigin(priorVisibleOrigin)
|
||||
tv.enclosingScrollView?.reflectScrolledClipView(clipView)
|
||||
}
|
||||
|
||||
// Update last highlighted state
|
||||
self.lastHighlightedText = textSnapshot
|
||||
self.lastLanguage = language
|
||||
|
|
@ -2871,6 +2902,7 @@ final class LineNumberedTextViewContainer: UIView {
|
|||
|
||||
struct CustomTextEditor: UIViewRepresentable {
|
||||
@Binding var text: String
|
||||
let documentID: UUID?
|
||||
let language: String
|
||||
let colorScheme: ColorScheme
|
||||
let fontSize: CGFloat
|
||||
|
|
@ -2962,8 +2994,16 @@ struct CustomTextEditor: UIViewRepresentable {
|
|||
func updateUIView(_ uiView: LineNumberedTextViewContainer, context: Context) {
|
||||
let textView = uiView.textView
|
||||
context.coordinator.parent = self
|
||||
let didSwitchDocument = context.coordinator.lastDocumentID != documentID
|
||||
let didFinishTabLoad = (context.coordinator.lastTabLoadingContent == true) && !isTabLoadingContent
|
||||
if didSwitchDocument {
|
||||
context.coordinator.lastDocumentID = documentID
|
||||
context.coordinator.cancelPendingBindingSync()
|
||||
context.coordinator.invalidateHighlightCache()
|
||||
}
|
||||
context.coordinator.lastTabLoadingContent = isTabLoadingContent
|
||||
if textView.text != text {
|
||||
let shouldPreferEditorBuffer = textView.isFirstResponder && !isTabLoadingContent
|
||||
let shouldPreferEditorBuffer = textView.isFirstResponder && !isTabLoadingContent && !didSwitchDocument && !didFinishTabLoad
|
||||
if shouldPreferEditorBuffer {
|
||||
context.coordinator.syncBindingTextImmediately(textView.text)
|
||||
} else {
|
||||
|
|
@ -3034,6 +3074,8 @@ struct CustomTextEditor: UIViewRepresentable {
|
|||
private var lastTranslucencyEnabled: Bool?
|
||||
private var isApplyingHighlight = false
|
||||
private var highlightGeneration: Int = 0
|
||||
var lastDocumentID: UUID?
|
||||
var lastTabLoadingContent: Bool?
|
||||
var hasPendingBindingSync: Bool { pendingBindingSync != nil }
|
||||
|
||||
init(_ parent: CustomTextEditor) {
|
||||
|
|
@ -3054,6 +3096,16 @@ struct CustomTextEditor: UIViewRepresentable {
|
|||
return parent.showLineNumbers && !parent.isLargeFileMode && !lineView.isHidden
|
||||
}
|
||||
|
||||
func invalidateHighlightCache() {
|
||||
lastHighlightedText = ""
|
||||
lastLanguage = nil
|
||||
lastColorScheme = nil
|
||||
lastLineHeight = nil
|
||||
lastHighlightToken = 0
|
||||
lastSelectionLocation = -1
|
||||
lastTranslucencyEnabled = nil
|
||||
}
|
||||
|
||||
private func syncBindingText(_ text: String, immediate: Bool = false) {
|
||||
if parent.isTabLoadingContent {
|
||||
return
|
||||
|
|
@ -3183,6 +3235,10 @@ struct CustomTextEditor: UIViewRepresentable {
|
|||
let selectionOnlyChange = text == lastHighlightedText &&
|
||||
styleStateUnchanged &&
|
||||
lastSelectionLocation != selectionLocation
|
||||
if selectionOnlyChange && parent.isLineWrapEnabled {
|
||||
lastSelectionLocation = selectionLocation
|
||||
return
|
||||
}
|
||||
if selectionOnlyChange && textLength >= EditorRuntimeLimits.cursorRehighlightMaxUTF16Length {
|
||||
lastSelectionLocation = selectionLocation
|
||||
return
|
||||
|
|
|
|||
|
|
@ -211,6 +211,14 @@ struct NeonSettingsView: View {
|
|||
String(format: NSLocalizedString(key, comment: ""), value)
|
||||
}
|
||||
|
||||
private var shouldShowSupportPurchaseControls: Bool {
|
||||
#if os(iOS)
|
||||
true
|
||||
#else
|
||||
supportPurchaseManager.canUseInAppPurchases
|
||||
#endif
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
settingsTabs
|
||||
#if os(macOS)
|
||||
|
|
@ -1122,47 +1130,47 @@ struct NeonSettingsView: View {
|
|||
Text(localized("Consumable support purchase. Can be purchased multiple times. No subscription and no auto-renewal."))
|
||||
.font(Typography.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
if supportPurchaseManager.canUseInAppPurchases {
|
||||
if shouldShowSupportPurchaseControls {
|
||||
VStack(alignment: .leading, spacing: UI.space8) {
|
||||
Text(localized("App Store Price"))
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
HStack(alignment: .firstTextBaseline, spacing: UI.space8) {
|
||||
Text(
|
||||
supportPurchaseManager.isLoadingProducts && supportPurchaseManager.supportProduct == nil
|
||||
? localized("Loading...")
|
||||
: localized("Current")
|
||||
)
|
||||
.font(Typography.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
Text(supportPurchaseManager.supportPriceLabel)
|
||||
.font(Typography.sectionTitle)
|
||||
.monospacedDigit()
|
||||
.accessibilityLabel(localized("App Store Price"))
|
||||
.accessibilityValue(
|
||||
supportPurchaseManager.supportProduct == nil
|
||||
? localized("Price unavailable")
|
||||
: supportPurchaseManager.supportPriceLabel
|
||||
)
|
||||
}
|
||||
if supportPurchaseManager.supportProduct == nil && !supportPurchaseManager.isLoadingProducts {
|
||||
Text(localized("Price unavailable right now. Tap Retry App Store."))
|
||||
.font(Typography.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
if let lastRefresh = supportPurchaseManager.lastSuccessfulPriceRefreshAt {
|
||||
Text(
|
||||
localized(
|
||||
"Last updated: %@",
|
||||
lastRefresh.formatted(date: .abbreviated, time: .shortened)
|
||||
)
|
||||
)
|
||||
Text(localized("App Store Price"))
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
HStack(alignment: .firstTextBaseline, spacing: UI.space8) {
|
||||
Text(
|
||||
supportPurchaseManager.isLoadingProducts && supportPurchaseManager.supportProduct == nil
|
||||
? localized("Loading...")
|
||||
: localized("Current")
|
||||
)
|
||||
.font(Typography.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.accessibilityLabel(localized("Last updated"))
|
||||
.accessibilityValue(lastRefresh.formatted(date: .abbreviated, time: .shortened))
|
||||
}
|
||||
Spacer()
|
||||
Text(supportPurchaseManager.supportPriceLabel)
|
||||
.font(Typography.sectionTitle)
|
||||
.monospacedDigit()
|
||||
.accessibilityLabel(localized("App Store Price"))
|
||||
.accessibilityValue(
|
||||
supportPurchaseManager.supportProduct == nil
|
||||
? localized("Price unavailable")
|
||||
: supportPurchaseManager.supportPriceLabel
|
||||
)
|
||||
}
|
||||
if supportPurchaseManager.supportProduct == nil && !supportPurchaseManager.isLoadingProducts {
|
||||
Text(localized("Price unavailable right now. Tap Retry App Store."))
|
||||
.font(Typography.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
if let lastRefresh = supportPurchaseManager.lastSuccessfulPriceRefreshAt {
|
||||
Text(
|
||||
localized(
|
||||
"Last updated: %@",
|
||||
lastRefresh.formatted(date: .abbreviated, time: .shortened)
|
||||
)
|
||||
)
|
||||
.font(Typography.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.accessibilityLabel(localized("Last updated"))
|
||||
.accessibilityValue(lastRefresh.formatted(date: .abbreviated, time: .shortened))
|
||||
}
|
||||
}
|
||||
.padding(UI.space12)
|
||||
.background(
|
||||
|
|
@ -1176,6 +1184,10 @@ struct NeonSettingsView: View {
|
|||
|
||||
HStack(spacing: UI.space12) {
|
||||
Button(supportPurchaseManager.isPurchasing ? localized("Purchasing…") : localized("Send Support Tip")) {
|
||||
guard supportPurchaseManager.canUseInAppPurchases else {
|
||||
Task { await supportPurchaseManager.purchaseSupport() }
|
||||
return
|
||||
}
|
||||
guard supportPurchaseManager.supportProduct != nil else {
|
||||
Task { await supportPurchaseManager.refreshPrice() }
|
||||
supportPurchaseManager.statusMessage = localized("Loading App Store product. Please try again in a moment.")
|
||||
|
|
@ -1186,8 +1198,6 @@ struct NeonSettingsView: View {
|
|||
.buttonStyle(.borderedProminent)
|
||||
.disabled(
|
||||
supportPurchaseManager.isPurchasing
|
||||
|| supportPurchaseManager.isLoadingProducts
|
||||
|| supportPurchaseManager.supportProduct == nil
|
||||
)
|
||||
|
||||
Button {
|
||||
|
|
@ -1206,6 +1216,15 @@ struct NeonSettingsView: View {
|
|||
.buttonStyle(.bordered)
|
||||
.disabled(supportPurchaseManager.isLoadingProducts)
|
||||
}
|
||||
|
||||
if !supportPurchaseManager.canUseInAppPurchases {
|
||||
Text(localized("Direct notarized builds are unaffected: all editor features stay fully available without any purchase."))
|
||||
.font(Typography.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(localized("Support purchase is available only in App Store/TestFlight builds."))
|
||||
.font(Typography.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} else {
|
||||
Text(localized("Direct notarized builds are unaffected: all editor features stay fully available without any purchase."))
|
||||
.font(Typography.footnote)
|
||||
|
|
|
|||
|
|
@ -325,6 +325,7 @@ struct WelcomeTourView: View {
|
|||
ToolbarItemInfo(title: "New Tab", description: "New Tab", shortcutMac: "Cmd+T", shortcutPad: "Cmd+T", iconName: "plus.square.on.square"),
|
||||
ToolbarItemInfo(title: "Open File…", description: "Open File…", shortcutMac: "Cmd+O", shortcutPad: "Cmd+O", iconName: "folder"),
|
||||
ToolbarItemInfo(title: "Save File", description: "Save File", shortcutMac: "Cmd+S", shortcutPad: "Cmd+S", iconName: "square.and.arrow.down"),
|
||||
ToolbarItemInfo(title: "Settings", description: "Settings", shortcutMac: "Cmd+", shortcutPad: "None", iconName: "gearshape"),
|
||||
ToolbarItemInfo(title: "Insert Template", description: "Insert Template for Current Language", shortcutMac: "None", shortcutPad: "None", iconName: "doc.badge.plus"),
|
||||
ToolbarItemInfo(title: "Language", description: "Language", shortcutMac: "None", shortcutPad: "None", iconName: "textformat"),
|
||||
ToolbarItemInfo(title: "AI Model & Settings", description: "AI Model & Settings", shortcutMac: "None", shortcutPad: "None", iconName: "brain.head.profile"),
|
||||
|
|
@ -489,9 +490,7 @@ struct WelcomeTourView: View {
|
|||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(
|
||||
supportPurchaseManager.isPurchasing
|
||||
|| supportPurchaseManager.isLoadingProducts
|
||||
|| !supportPurchaseManager.canUseInAppPurchases
|
||||
shouldDisableSupportPurchaseButton
|
||||
)
|
||||
|
||||
if let status = supportPurchaseManager.statusMessage, !status.isEmpty {
|
||||
|
|
@ -501,7 +500,7 @@ struct WelcomeTourView: View {
|
|||
}
|
||||
|
||||
if !supportPurchaseManager.canUseInAppPurchases {
|
||||
Text("Purchase is available in App Store/TestFlight builds.")
|
||||
Text(NSLocalizedString("Support purchase is available only in App Store/TestFlight builds.", comment: ""))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
|
@ -514,6 +513,16 @@ struct WelcomeTourView: View {
|
|||
)
|
||||
}
|
||||
|
||||
private var shouldDisableSupportPurchaseButton: Bool {
|
||||
#if os(iOS)
|
||||
supportPurchaseManager.isPurchasing
|
||||
#else
|
||||
supportPurchaseManager.isPurchasing
|
||||
|| supportPurchaseManager.isLoadingProducts
|
||||
|| !supportPurchaseManager.canUseInAppPurchases
|
||||
#endif
|
||||
}
|
||||
|
||||
private func toolbarGrid(items: [ToolbarItemInfo]) -> some View {
|
||||
return GeometryReader { proxy in
|
||||
let isCompact = proxy.size.width < 640
|
||||
|
|
|
|||
|
|
@ -24,15 +24,18 @@
|
|||
"Unavailable" = "Nicht verfügbar";
|
||||
"Support purchase bypass enabled for TestFlight/Sandbox testing." = "Support-Kauf-Bypass für TestFlight/Sandbox aktiviert.";
|
||||
"App Store pricing is only available in App Store/TestFlight builds." = "App-Store-Preise sind nur in App-Store-/TestFlight-Builds verfügbar.";
|
||||
"App Store did not return product %@. Check App Store Connect and TestFlight availability." = "App Store hat das Produkt %@ nicht zurückgegeben. Prüfe App Store Connect und die TestFlight-Verfügbarkeit.";
|
||||
"Could not load App Store product. Please try again." = "App-Store-Produkt konnte nicht geladen werden. Bitte erneut versuchen.";
|
||||
"Failed to load App Store products: %@" = "App-Store-Produkte konnten nicht geladen werden: %@";
|
||||
"In-app purchase is only available in App Store/TestFlight builds." = "In-App-Kauf ist nur in App-Store-/TestFlight-Builds verfügbar.";
|
||||
"In-App Purchases are disabled on this device. Check App Store login and Screen Time restrictions." = "In-App-Käufe sind auf diesem Gerät deaktiviert. Prüfe App-Store-Login und Bildschirmzeit-Beschränkungen.";
|
||||
"Support purchase is currently unavailable." = "Support-Kauf ist derzeit nicht verfügbar.";
|
||||
"Thank you for supporting Neon Vision Editor." = "Danke für deine Unterstützung von Neon Vision Editor.";
|
||||
"Purchase is pending approval." = "Der Kauf wartet auf Genehmigung.";
|
||||
"Purchase canceled." = "Kauf abgebrochen.";
|
||||
"Purchase did not complete." = "Der Kauf wurde nicht abgeschlossen.";
|
||||
"Purchase failed: %@" = "Kauf fehlgeschlagen: %@";
|
||||
"Purchase failed: %@ (%@)" = "Kauf fehlgeschlagen: %@ (%@)";
|
||||
"Transaction verification failed." = "Transaktionsprüfung fehlgeschlagen.";
|
||||
|
||||
"General" = "Allgemein";
|
||||
|
|
|
|||
|
|
@ -24,15 +24,18 @@
|
|||
"Unavailable" = "Unavailable";
|
||||
"Support purchase bypass enabled for TestFlight/Sandbox testing." = "Support purchase bypass enabled for TestFlight/Sandbox testing.";
|
||||
"App Store pricing is only available in App Store/TestFlight builds." = "App Store pricing is only available in App Store/TestFlight builds.";
|
||||
"App Store did not return product %@. Check App Store Connect and TestFlight availability." = "App Store did not return product %@. Check App Store Connect and TestFlight availability.";
|
||||
"Could not load App Store product. Please try again." = "Could not load App Store product. Please try again.";
|
||||
"Failed to load App Store products: %@" = "Failed to load App Store products: %@";
|
||||
"In-app purchase is only available in App Store/TestFlight builds." = "In-app purchase is only available in App Store/TestFlight builds.";
|
||||
"In-App Purchases are disabled on this device. Check App Store login and Screen Time restrictions." = "In-App Purchases are disabled on this device. Check App Store login and Screen Time restrictions.";
|
||||
"Support purchase is currently unavailable." = "Support purchase is currently unavailable.";
|
||||
"Thank you for supporting Neon Vision Editor." = "Thank you for supporting Neon Vision Editor.";
|
||||
"Purchase is pending approval." = "Purchase is pending approval.";
|
||||
"Purchase canceled." = "Purchase canceled.";
|
||||
"Purchase did not complete." = "Purchase did not complete.";
|
||||
"Purchase failed: %@" = "Purchase failed: %@";
|
||||
"Purchase failed: %@ (%@)" = "Purchase failed: %@ (%@)";
|
||||
"Transaction verification failed." = "Transaction verification failed.";
|
||||
|
||||
"General" = "General";
|
||||
|
|
|
|||
Loading…
Reference in a new issue