feat: tip IAP, commercial GitHub Pages, and App Store screenshots
Add optional StoreKit support tip (features stay free), redesign the landing page with zh/en + light/dark and product shots, and ship iPhone/iPad/Mac App Store screenshot sets.
This commit is contained in:
@@ -38,8 +38,9 @@ struct DashboardView: View {
|
||||
.padding(.top, Spacing.sm)
|
||||
|
||||
// Leftover window height splits evenly above / below the mic
|
||||
// bar so spacing stays balanced at any window size.
|
||||
Spacer(minLength: Spacing.xs)
|
||||
// bar so spacing stays balanced at any window size. The top
|
||||
// gap keeps a larger floor so the mic never hugs the canvas.
|
||||
Spacer(minLength: 30)
|
||||
|
||||
BottomDictationBar(viewModel: viewModel)
|
||||
.padding(.horizontal, MacMetrics.pageHorizontalInset)
|
||||
|
||||
@@ -261,13 +261,20 @@ struct MacOnboardingView: View {
|
||||
@ViewBuilder
|
||||
private var heroIcon: some View {
|
||||
if model.step == .welcome {
|
||||
Image("OSGBrandMark")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 128, height: 128)
|
||||
.foregroundStyle(colorScheme == .dark ? Color.white : palette.accent)
|
||||
.accessibilityLabel("OSGKeyboard")
|
||||
ZStack {
|
||||
SonicParticleField()
|
||||
.frame(width: 300, height: 300)
|
||||
|
||||
Image("OSGBrandMark")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 128, height: 128)
|
||||
.foregroundStyle(colorScheme == .dark ? Color.white : palette.accent)
|
||||
.accessibilityLabel("OSGKeyboard")
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
.frame(width: 300, height: 300)
|
||||
} else {
|
||||
ZStack {
|
||||
Circle()
|
||||
|
||||
@@ -39,6 +39,7 @@ struct MacSettingsView: View {
|
||||
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: Spacing.xl) {
|
||||
supportSection
|
||||
generalSection
|
||||
recognitionSection
|
||||
if viewModel.config.engineMode == "cloud" {
|
||||
@@ -298,6 +299,17 @@ struct MacSettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Support developer
|
||||
|
||||
private var supportSection: some View {
|
||||
MacSettingsSection(
|
||||
title: MacL10n.string("tip.title", language: lang),
|
||||
footer: MacL10n.string("tip.consumableNotice", language: lang)
|
||||
) {
|
||||
MacSupportDeveloperTipRows(language: lang)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Legal
|
||||
|
||||
private var legalSection: some View {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// MacSupportDeveloperTipRows.swift
|
||||
// OSGKeyboard · Mac
|
||||
//
|
||||
// Optional consumable tip rows for macOS Settings.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MacSupportDeveloperTipRows: View {
|
||||
@ObservedObject private var tipManager = TipPurchaseManager.shared
|
||||
@Environment(\.themePalette) private var palette
|
||||
|
||||
let language: AppUILanguage
|
||||
|
||||
@State private var showThankYouAlert = false
|
||||
@State private var showErrorAlert = false
|
||||
@State private var errorMessage = ""
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: MacMetrics.settingsRowGap) {
|
||||
SupportDeveloperTipBody(language: language)
|
||||
.padding(.horizontal, MacMetrics.settingsCardInset)
|
||||
|
||||
if tipManager.supportCount > 0 {
|
||||
Text(
|
||||
SharedL10n.format(
|
||||
"tip.thankYou.past",
|
||||
language: language,
|
||||
tipManager.supportCount
|
||||
)
|
||||
)
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(palette.accent)
|
||||
.padding(.horizontal, MacMetrics.settingsCardInset)
|
||||
}
|
||||
|
||||
MacSettingsToolButton(title: tipButtonTitle, disabled: isPurchaseInFlight) {
|
||||
Task { await tipManager.purchase() }
|
||||
}
|
||||
.padding(.horizontal, MacMetrics.settingsCardInset)
|
||||
}
|
||||
.onChange(of: tipManager.purchaseState) { _, newValue in
|
||||
switch newValue {
|
||||
case .succeeded:
|
||||
showThankYouAlert = true
|
||||
case .failed(let message):
|
||||
errorMessage = message
|
||||
showErrorAlert = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
.alert(
|
||||
SharedL10n.string("tip.thankYou.title", language: language),
|
||||
isPresented: $showThankYouAlert
|
||||
) {
|
||||
Button(SharedL10n.string("tip.alert.dismiss", language: language)) {
|
||||
tipManager.acknowledgePurchaseState()
|
||||
}
|
||||
} message: {
|
||||
Text(SharedL10n.string("tip.thankYou.message", language: language))
|
||||
}
|
||||
.alert(
|
||||
SharedL10n.string("tip.error.title", language: language),
|
||||
isPresented: $showErrorAlert
|
||||
) {
|
||||
Button(SharedL10n.string("tip.alert.dismiss", language: language)) {
|
||||
tipManager.acknowledgePurchaseState()
|
||||
}
|
||||
} message: {
|
||||
Text(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private var isPurchaseInFlight: Bool {
|
||||
switch tipManager.purchaseState {
|
||||
case .loading, .purchasing:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private var tipButtonTitle: String {
|
||||
if let product = tipManager.product {
|
||||
return SharedL10n.format("tip.button", language: language, product.displayPrice)
|
||||
}
|
||||
return SharedL10n.string("tip.buttonFallback", language: language)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user