From da9a4ce68b957b991f678358dd12e45ae3031b15 Mon Sep 17 00:00:00 2001 From: h3p Date: Wed, 25 Feb 2026 01:21:58 +0100 Subject: [PATCH] Fix support tip IAP in TestFlight --- .gitignore | 1 + Neon Vision Editor.xcodeproj/project.pbxproj | 6 +- .../Data/SupportPurchaseManager.swift | 68 ++++++++- Neon Vision Editor/SupportOptional.storekit | 2 +- Neon Vision Editor/UI/ContentView.swift | 1 + Neon Vision Editor/UI/EditorTextView.swift | 132 +++++++++++++----- Neon Vision Editor/UI/NeonSettingsView.swift | 99 +++++++------ Neon Vision Editor/UI/PanelsAndHelpers.swift | 17 ++- .../de.lproj/Localizable.strings | 3 + .../en.lproj/Localizable.strings | 3 + 10 files changed, 239 insertions(+), 93 deletions(-) diff --git a/.gitignore b/.gitignore index 42b1079..a8b1612 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ # Xcode DerivedData/ .DerivedData/ +.DerivedData*/ DerivedData-*/ *.xcresult *.xcuserstate diff --git a/Neon Vision Editor.xcodeproj/project.pbxproj b/Neon Vision Editor.xcodeproj/project.pbxproj index 3b064bc..a593235 100644 --- a/Neon Vision Editor.xcodeproj/project.pbxproj +++ b/Neon Vision Editor.xcodeproj/project.pbxproj @@ -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; diff --git a/Neon Vision Editor/Data/SupportPurchaseManager.swift b/Neon Vision Editor/Data/SupportPurchaseManager.swift index 75fbd08..2592165 100644 --- a/Neon Vision Editor/Data/SupportPurchaseManager.swift +++ b/Neon Vision Editor/Data/SupportPurchaseManager.swift @@ -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. diff --git a/Neon Vision Editor/SupportOptional.storekit b/Neon Vision Editor/SupportOptional.storekit index 9e6e323..bed9dbc 100644 --- a/Neon Vision Editor/SupportOptional.storekit +++ b/Neon Vision Editor/SupportOptional.storekit @@ -20,7 +20,7 @@ "locale" : "de_DE" } ], - "productID" : "h3p.neon-vision-editor.support.optional", + "productID" : "002420160", "referenceName" : "Optional Support Tip", "type" : "Consumable" } diff --git a/Neon Vision Editor/UI/ContentView.swift b/Neon Vision Editor/UI/ContentView.swift index 3452720..0c162eb 100644 --- a/Neon Vision Editor/UI/ContentView.swift +++ b/Neon Vision Editor/UI/ContentView.swift @@ -2782,6 +2782,7 @@ struct ContentView: View { // Single editor (no TabView) CustomTextEditor( text: currentContentBinding, + documentID: viewModel.selectedTabID, language: currentLanguage, colorScheme: colorScheme, fontSize: editorFontSize, diff --git a/Neon Vision Editor/UI/EditorTextView.swift b/Neon Vision Editor/UI/EditorTextView.swift index c422170..cba5cde 100644 --- a/Neon Vision Editor/UI/EditorTextView.swift +++ b/Neon Vision Editor/UI/EditorTextView.swift @@ -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 diff --git a/Neon Vision Editor/UI/NeonSettingsView.swift b/Neon Vision Editor/UI/NeonSettingsView.swift index 26f5b52..591b801 100644 --- a/Neon Vision Editor/UI/NeonSettingsView.swift +++ b/Neon Vision Editor/UI/NeonSettingsView.swift @@ -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) diff --git a/Neon Vision Editor/UI/PanelsAndHelpers.swift b/Neon Vision Editor/UI/PanelsAndHelpers.swift index c0a95fe..f1ac873 100644 --- a/Neon Vision Editor/UI/PanelsAndHelpers.swift +++ b/Neon Vision Editor/UI/PanelsAndHelpers.swift @@ -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 diff --git a/Neon Vision Editor/de.lproj/Localizable.strings b/Neon Vision Editor/de.lproj/Localizable.strings index 4189037..7b97202 100644 --- a/Neon Vision Editor/de.lproj/Localizable.strings +++ b/Neon Vision Editor/de.lproj/Localizable.strings @@ -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"; diff --git a/Neon Vision Editor/en.lproj/Localizable.strings b/Neon Vision Editor/en.lproj/Localizable.strings index 8ec7246..7db8f7f 100644 --- a/Neon Vision Editor/en.lproj/Localizable.strings +++ b/Neon Vision Editor/en.lproj/Localizable.strings @@ -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";