Fix support tip IAP in TestFlight

This commit is contained in:
h3p 2026-02-25 01:21:58 +01:00
parent 960e1e8466
commit da9a4ce68b
10 changed files with 239 additions and 93 deletions

1
.gitignore vendored
View file

@ -4,6 +4,7 @@
# Xcode
DerivedData/
.DerivedData/
.DerivedData*/
DerivedData-*/
*.xcresult
*.xcuserstate

View file

@ -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;

View file

@ -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.

View file

@ -20,7 +20,7 @@
"locale" : "de_DE"
}
],
"productID" : "h3p.neon-vision-editor.support.optional",
"productID" : "002420160",
"referenceName" : "Optional Support Tip",
"type" : "Consumable"
}

View file

@ -2782,6 +2782,7 @@ struct ContentView: View {
// Single editor (no TabView)
CustomTextEditor(
text: currentContentBinding,
documentID: viewModel.selectedTabID,
language: currentLanguage,
colorScheme: colorScheme,
fontSize: editorFontSize,

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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";

View file

@ -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";