|
|
@@ -71,203 +71,310 @@ struct PremiumFeaturesView: View {
|
|
|
)
|
|
|
.ignoresSafeArea()
|
|
|
|
|
|
- ScrollView {
|
|
|
- VStack(alignment: .leading, spacing: 14) {
|
|
|
- HStack {
|
|
|
- Button(action: { dismissPremiumUI() }) {
|
|
|
- Image(systemName: "xmark")
|
|
|
- .font(.system(size: 13, weight: .semibold))
|
|
|
- .foregroundStyle(primaryTextColor)
|
|
|
- }
|
|
|
- .buttonStyle(.plain)
|
|
|
-
|
|
|
- Spacer()
|
|
|
-
|
|
|
- Button {
|
|
|
- Task { await premiumStore.restorePurchases() }
|
|
|
- } label: {
|
|
|
- HStack(spacing: 6) {
|
|
|
- if premiumStore.restoreInProgress {
|
|
|
- ProgressView()
|
|
|
- .controlSize(.mini)
|
|
|
- }
|
|
|
- Text(premiumStore.restoreInProgress ? "Restoring..." : "Restore")
|
|
|
- .font(.system(size: 11, weight: .semibold))
|
|
|
- }
|
|
|
- .foregroundStyle(primaryTextColor)
|
|
|
- .padding(.horizontal, 8)
|
|
|
- .padding(.vertical, 3)
|
|
|
- .background(
|
|
|
- RoundedRectangle(cornerRadius: 7, style: .continuous)
|
|
|
- .fill(chipFillColor)
|
|
|
- )
|
|
|
- }
|
|
|
- .buttonStyle(.plain)
|
|
|
- .disabled(premiumStore.purchaseInProgress || premiumStore.restoreInProgress)
|
|
|
- }
|
|
|
-
|
|
|
- Text("Upgrade to unlock all Premium Features")
|
|
|
- .font(.system(size: 20, weight: .bold))
|
|
|
- .foregroundStyle(primaryTextColor)
|
|
|
- .lineSpacing(1)
|
|
|
+ GeometryReader { geo in
|
|
|
+ let useWideLayout = geo.size.width >= 620
|
|
|
+ VStack(spacing: 0) {
|
|
|
+ paywallTopBar
|
|
|
+ .padding(.horizontal, useWideLayout ? 22 : 16)
|
|
|
+ .padding(.vertical, 12)
|
|
|
|
|
|
- if let err = premiumStore.loadError {
|
|
|
- Text(err)
|
|
|
- .font(.system(size: 11, weight: .medium))
|
|
|
- .foregroundStyle(.orange.opacity(0.95))
|
|
|
+ if useWideLayout {
|
|
|
+ desktopSplitContent
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
+ } else {
|
|
|
+ compactScrollContent
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
}
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .task {
|
|
|
+ await premiumStore.loadProducts()
|
|
|
+ }
|
|
|
+ .alert("Payment Successful", isPresented: $showingSuccessAlert) {
|
|
|
+ Button("Done") { dismissPremiumUI() }
|
|
|
+ } message: {
|
|
|
+ Text("Your \(selectedPlan == .perpetual ? "Lifetime" : "Yearly") plan has been activated.")
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if let err = premiumStore.purchaseError {
|
|
|
- Text(err)
|
|
|
- .font(.system(size: 11, weight: .medium))
|
|
|
- .foregroundStyle(.orange.opacity(0.95))
|
|
|
- }
|
|
|
+ private var paywallTopBar: some View {
|
|
|
+ HStack {
|
|
|
+ Button(action: { dismissPremiumUI() }) {
|
|
|
+ Image(systemName: "xmark")
|
|
|
+ .font(.system(size: 13, weight: .semibold))
|
|
|
+ .foregroundStyle(primaryTextColor)
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
|
|
|
- if let restoreMessage = premiumStore.restoreMessage {
|
|
|
- Text(restoreMessage)
|
|
|
- .font(.system(size: 11, weight: .medium))
|
|
|
- .foregroundStyle(.green.opacity(0.95))
|
|
|
- }
|
|
|
+ Spacer()
|
|
|
|
|
|
- LazyVGrid(
|
|
|
- columns: [GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 10)],
|
|
|
- spacing: 10
|
|
|
- ) {
|
|
|
- ForEach(Array(featureRows.enumerated()), id: \.offset) { index, feature in
|
|
|
- featureCard(title: feature.0, subtitle: feature.1, color: cardColors[index % cardColors.count])
|
|
|
- }
|
|
|
+ Button {
|
|
|
+ Task { await premiumStore.restorePurchases() }
|
|
|
+ } label: {
|
|
|
+ HStack(spacing: 6) {
|
|
|
+ if premiumStore.restoreInProgress {
|
|
|
+ ProgressView()
|
|
|
+ .controlSize(.mini)
|
|
|
}
|
|
|
+ Text(premiumStore.restoreInProgress ? "Restoring…" : "Restore")
|
|
|
+ .font(.system(size: 12, weight: .semibold))
|
|
|
+ }
|
|
|
+ .foregroundStyle(primaryTextColor)
|
|
|
+ .padding(.horizontal, 11)
|
|
|
+ .padding(.vertical, 5)
|
|
|
+ .background(
|
|
|
+ RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
|
+ .fill(chipFillColor)
|
|
|
+ )
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ .disabled(premiumStore.purchaseInProgress || premiumStore.restoreInProgress)
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
+ private var desktopSplitContent: some View {
|
|
|
+ HStack(alignment: .top, spacing: 0) {
|
|
|
+ ScrollView {
|
|
|
+ VStack(alignment: .leading, spacing: 18) {
|
|
|
+ headlineBlock(useLargeType: true)
|
|
|
+ messagesBlock
|
|
|
+ featureGrid(desktop: true)
|
|
|
if productsUnavailable {
|
|
|
- VStack(alignment: .leading, spacing: 10) {
|
|
|
- Text("Store products not loaded")
|
|
|
- .font(.system(size: 14, weight: .bold))
|
|
|
- .foregroundStyle(primaryTextColor)
|
|
|
- Text(
|
|
|
- "Prices and checkout need product metadata from App Store Connect, or a StoreKit Configuration file when running from Xcode. Product IDs must match exactly."
|
|
|
- )
|
|
|
- .font(.system(size: 11, weight: .medium))
|
|
|
- .foregroundStyle(secondaryTextColor)
|
|
|
- .fixedSize(horizontal: false, vertical: true)
|
|
|
- Button {
|
|
|
- Task { await premiumStore.loadProducts() }
|
|
|
- } label: {
|
|
|
- Text("Retry")
|
|
|
- .font(.system(size: 13, weight: .semibold))
|
|
|
- .foregroundStyle(.white)
|
|
|
- .frame(maxWidth: .infinity)
|
|
|
- .padding(.vertical, 10)
|
|
|
- .background(RoundedRectangle(cornerRadius: 10, style: .continuous).fill(Color.orange.opacity(0.85)))
|
|
|
- }
|
|
|
- .buttonStyle(.plain)
|
|
|
- }
|
|
|
- .padding(14)
|
|
|
- .frame(maxWidth: .infinity, alignment: .leading)
|
|
|
- .background(
|
|
|
- RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
|
- .fill(Color.orange.opacity(0.18))
|
|
|
- )
|
|
|
- .overlay(
|
|
|
- RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
|
- .stroke(Color.orange.opacity(0.45), lineWidth: 1)
|
|
|
- )
|
|
|
+ productsUnavailableBanner
|
|
|
}
|
|
|
+ }
|
|
|
+ .padding(.leading, 22)
|
|
|
+ .padding(.trailing, 14)
|
|
|
+ .padding(.bottom, 22)
|
|
|
+ }
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
|
|
|
- priceCard(
|
|
|
- title: "Lifetime",
|
|
|
- price: priceLine(for: .perpetual),
|
|
|
- subtitle: "Own the software for lifetime",
|
|
|
- badgeText: "One-time payment",
|
|
|
- isSelected: selectedPlan == .perpetual
|
|
|
- )
|
|
|
- .onTapGesture {
|
|
|
- selectedPlan = .perpetual
|
|
|
- }
|
|
|
+ Rectangle()
|
|
|
+ .fill(paywallDividerColor)
|
|
|
+ .frame(width: 1)
|
|
|
+ .padding(.vertical, 12)
|
|
|
|
|
|
- priceCard(
|
|
|
- title: "Yearly",
|
|
|
- price: priceLine(for: .yearly),
|
|
|
- subtitle: "Billed annually",
|
|
|
- badgeText: nil,
|
|
|
- isSelected: selectedPlan == .yearly
|
|
|
- )
|
|
|
- .onTapGesture {
|
|
|
- selectedPlan = .yearly
|
|
|
- }
|
|
|
+ ZStack(alignment: .topLeading) {
|
|
|
+ Rectangle()
|
|
|
+ .fill(colorScheme == .dark ? Color.white.opacity(0.06) : Color.black.opacity(0.035))
|
|
|
|
|
|
- Button(action: {
|
|
|
- guard let product = selectedProduct else { return }
|
|
|
- Task {
|
|
|
- let ok = await premiumStore.purchase(product)
|
|
|
- if ok {
|
|
|
- showingSuccessAlert = true
|
|
|
- }
|
|
|
- }
|
|
|
- }) {
|
|
|
- HStack {
|
|
|
- if premiumStore.purchaseInProgress {
|
|
|
- ProgressView()
|
|
|
- .controlSize(.small)
|
|
|
- .tint(.white)
|
|
|
- }
|
|
|
- Text("Continue")
|
|
|
- .font(.system(size: 16, weight: .bold))
|
|
|
- }
|
|
|
- .foregroundStyle(.white)
|
|
|
- .frame(maxWidth: .infinity)
|
|
|
- .padding(.vertical, 12)
|
|
|
- .background(
|
|
|
- RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
|
- .fill(continueEnabled ? Color.blue.opacity(0.9) : Color.gray.opacity(0.55))
|
|
|
- )
|
|
|
- }
|
|
|
- .buttonStyle(.plain)
|
|
|
- .disabled(!continueEnabled)
|
|
|
- .padding(.top, 6)
|
|
|
-
|
|
|
- HStack {
|
|
|
- Spacer()
|
|
|
- Button("Privacy Policy") {
|
|
|
- openExternalPage(ExternalPage.privacy)
|
|
|
- }
|
|
|
- .buttonStyle(.plain)
|
|
|
- .font(.system(size: 11, weight: .medium))
|
|
|
- .foregroundStyle(secondaryTextColor)
|
|
|
-
|
|
|
- Spacer()
|
|
|
-
|
|
|
- Button("Support") {
|
|
|
- openExternalPage(ExternalPage.support)
|
|
|
- }
|
|
|
- .buttonStyle(.plain)
|
|
|
- .font(.system(size: 11, weight: .medium))
|
|
|
- .foregroundStyle(secondaryTextColor)
|
|
|
-
|
|
|
- Spacer()
|
|
|
-
|
|
|
- Button("Terms of Service") {
|
|
|
- openExternalPage(ExternalPage.terms)
|
|
|
- }
|
|
|
- .buttonStyle(.plain)
|
|
|
- .font(.system(size: 11, weight: .medium))
|
|
|
- .foregroundStyle(secondaryTextColor)
|
|
|
- Spacer()
|
|
|
- }
|
|
|
- .padding(.top, 2)
|
|
|
+ VStack(alignment: .leading, spacing: 18) {
|
|
|
+ Text("Choose a plan")
|
|
|
+ .font(.system(size: 15, weight: .semibold))
|
|
|
+ .foregroundStyle(primaryTextColor)
|
|
|
+
|
|
|
+ planCardsStack(compactPricing: false)
|
|
|
+
|
|
|
+ continuePurchaseButton
|
|
|
+
|
|
|
+ sidebarFooterLinks
|
|
|
}
|
|
|
- .padding(16)
|
|
|
+ .padding(.leading, 18)
|
|
|
+ .padding(.trailing, 22)
|
|
|
+ .padding(.bottom, 22)
|
|
|
}
|
|
|
+ .frame(width: 288)
|
|
|
+ .frame(maxHeight: .infinity, alignment: .top)
|
|
|
}
|
|
|
- .task {
|
|
|
- await premiumStore.loadProducts()
|
|
|
+ .frame(maxHeight: .infinity)
|
|
|
+ }
|
|
|
+
|
|
|
+ private var compactScrollContent: some View {
|
|
|
+ ScrollView {
|
|
|
+ VStack(alignment: .leading, spacing: 16) {
|
|
|
+ headlineBlock(useLargeType: false)
|
|
|
+ messagesBlock
|
|
|
+ featureGrid(desktop: false)
|
|
|
+ if productsUnavailable {
|
|
|
+ productsUnavailableBanner
|
|
|
+ }
|
|
|
+ planCardsStack(compactPricing: true)
|
|
|
+ continuePurchaseButton
|
|
|
+ compactFooterLinks
|
|
|
+ }
|
|
|
+ .padding(.horizontal, 16)
|
|
|
+ .padding(.bottom, 20)
|
|
|
}
|
|
|
- .alert("Payment Successful", isPresented: $showingSuccessAlert) {
|
|
|
- Button("Done") { dismissPremiumUI() }
|
|
|
- } message: {
|
|
|
- Text("Your \(selectedPlan == .perpetual ? "Lifetime" : "Yearly") plan has been activated.")
|
|
|
+ }
|
|
|
+
|
|
|
+ private func headlineBlock(useLargeType: Bool) -> some View {
|
|
|
+ VStack(alignment: .leading, spacing: 6) {
|
|
|
+ Text("Premium")
|
|
|
+ .font(.system(size: useLargeType ? 28 : 22, weight: .bold))
|
|
|
+ .foregroundStyle(primaryTextColor)
|
|
|
+ Text("Unlock every feature across the launcher, widgets, and in‑app browser.")
|
|
|
+ .font(.system(size: useLargeType ? 14 : 13, weight: .medium))
|
|
|
+ .foregroundStyle(secondaryTextColor)
|
|
|
+ .fixedSize(horizontal: false, vertical: true)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private var messagesBlock: some View {
|
|
|
+ VStack(alignment: .leading, spacing: 6) {
|
|
|
+ if let err = premiumStore.loadError {
|
|
|
+ Text(err)
|
|
|
+ .font(.system(size: 12, weight: .medium))
|
|
|
+ .foregroundStyle(.orange.opacity(0.95))
|
|
|
+ }
|
|
|
+ if let err = premiumStore.purchaseError {
|
|
|
+ Text(err)
|
|
|
+ .font(.system(size: 12, weight: .medium))
|
|
|
+ .foregroundStyle(.orange.opacity(0.95))
|
|
|
+ }
|
|
|
+ if let restoreMessage = premiumStore.restoreMessage {
|
|
|
+ Text(restoreMessage)
|
|
|
+ .font(.system(size: 12, weight: .medium))
|
|
|
+ .foregroundStyle(.green.opacity(0.95))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private var productsUnavailableBanner: some View {
|
|
|
+ VStack(alignment: .leading, spacing: 10) {
|
|
|
+ Text("Store products not loaded")
|
|
|
+ .font(.system(size: 14, weight: .bold))
|
|
|
+ .foregroundStyle(primaryTextColor)
|
|
|
+ Text(
|
|
|
+ "Prices and checkout need product metadata from App Store Connect, or a StoreKit Configuration file when running from Xcode. Product IDs must match exactly."
|
|
|
+ )
|
|
|
+ .font(.system(size: 12, weight: .medium))
|
|
|
+ .foregroundStyle(secondaryTextColor)
|
|
|
+ .fixedSize(horizontal: false, vertical: true)
|
|
|
+ Button {
|
|
|
+ Task { await premiumStore.loadProducts() }
|
|
|
+ } label: {
|
|
|
+ Text("Retry")
|
|
|
+ .font(.system(size: 13, weight: .semibold))
|
|
|
+ .foregroundStyle(.white)
|
|
|
+ .frame(maxWidth: .infinity)
|
|
|
+ .padding(.vertical, 10)
|
|
|
+ .background(RoundedRectangle(cornerRadius: 10, style: .continuous).fill(Color.orange.opacity(0.85)))
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ }
|
|
|
+ .padding(14)
|
|
|
+ .frame(maxWidth: .infinity, alignment: .leading)
|
|
|
+ .background(
|
|
|
+ RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
|
+ .fill(Color.orange.opacity(0.18))
|
|
|
+ )
|
|
|
+ .overlay(
|
|
|
+ RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
|
+ .stroke(Color.orange.opacity(0.45), lineWidth: 1)
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ private func featureGrid(desktop: Bool) -> some View {
|
|
|
+ let spacing: CGFloat = desktop ? 14 : 10
|
|
|
+ let columns = [
|
|
|
+ GridItem(.flexible(), spacing: spacing),
|
|
|
+ GridItem(.flexible(), spacing: spacing),
|
|
|
+ ]
|
|
|
+ return LazyVGrid(columns: columns, spacing: desktop ? 12 : 10) {
|
|
|
+ ForEach(Array(featureRows.enumerated()), id: \.offset) { index, feature in
|
|
|
+ featureCard(
|
|
|
+ title: feature.0,
|
|
|
+ subtitle: feature.1,
|
|
|
+ color: cardColors[index % cardColors.count],
|
|
|
+ desktop: desktop
|
|
|
+ )
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private func planCardsStack(compactPricing: Bool) -> some View {
|
|
|
+ VStack(spacing: 10) {
|
|
|
+ priceCard(
|
|
|
+ title: "Lifetime",
|
|
|
+ price: priceLine(for: .perpetual),
|
|
|
+ subtitle: "Pay once — no subscription.",
|
|
|
+ badgeText: "One-time payment",
|
|
|
+ isSelected: selectedPlan == .perpetual,
|
|
|
+ compact: compactPricing
|
|
|
+ )
|
|
|
+ .onTapGesture { selectedPlan = .perpetual }
|
|
|
+
|
|
|
+ priceCard(
|
|
|
+ title: "Yearly",
|
|
|
+ price: priceLine(for: .yearly),
|
|
|
+ subtitle: "Billed annually. Cancel anytime.",
|
|
|
+ badgeText: nil,
|
|
|
+ isSelected: selectedPlan == .yearly,
|
|
|
+ compact: compactPricing
|
|
|
+ )
|
|
|
+ .onTapGesture { selectedPlan = .yearly }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private var continuePurchaseButton: some View {
|
|
|
+ Button(action: {
|
|
|
+ guard let product = selectedProduct else { return }
|
|
|
+ Task {
|
|
|
+ let ok = await premiumStore.purchase(product)
|
|
|
+ if ok {
|
|
|
+ showingSuccessAlert = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }) {
|
|
|
+ HStack {
|
|
|
+ if premiumStore.purchaseInProgress {
|
|
|
+ ProgressView()
|
|
|
+ .controlSize(.small)
|
|
|
+ .tint(.white)
|
|
|
+ }
|
|
|
+ Text("Continue")
|
|
|
+ .font(.system(size: 15, weight: .bold))
|
|
|
+ }
|
|
|
+ .foregroundStyle(.white)
|
|
|
+ .frame(maxWidth: .infinity)
|
|
|
+ .padding(.vertical, 12)
|
|
|
+ .background(
|
|
|
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
|
+ .fill(continueEnabled ? Color.blue.opacity(0.9) : Color.gray.opacity(0.55))
|
|
|
+ )
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ .disabled(!continueEnabled)
|
|
|
+ .padding(.top, 2)
|
|
|
+ }
|
|
|
+
|
|
|
+ private var sidebarFooterLinks: some View {
|
|
|
+ VStack(alignment: .leading, spacing: 8) {
|
|
|
+ linkButton("Privacy Policy", ExternalPage.privacy)
|
|
|
+ linkButton("Support", ExternalPage.support)
|
|
|
+ linkButton("Terms of Service", ExternalPage.terms)
|
|
|
+ }
|
|
|
+ .frame(maxWidth: .infinity, alignment: .leading)
|
|
|
+ .padding(.top, 4)
|
|
|
+ }
|
|
|
+
|
|
|
+ private var compactFooterLinks: some View {
|
|
|
+ HStack {
|
|
|
+ Spacer()
|
|
|
+ linkButton("Privacy Policy", ExternalPage.privacy)
|
|
|
+ Spacer()
|
|
|
+ linkButton("Support", ExternalPage.support)
|
|
|
+ Spacer()
|
|
|
+ linkButton("Terms", ExternalPage.terms)
|
|
|
+ Spacer()
|
|
|
+ }
|
|
|
+ .padding(.top, 4)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func linkButton(_ title: String, _ url: String) -> some View {
|
|
|
+ Button(title) {
|
|
|
+ openExternalPage(url)
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ .font(.system(size: 11.5, weight: .medium))
|
|
|
+ .foregroundStyle(secondaryTextColor)
|
|
|
+ }
|
|
|
+
|
|
|
+ private var paywallDividerColor: Color {
|
|
|
+ colorScheme == .dark ? Color.white.opacity(0.12) : Color.black.opacity(0.08)
|
|
|
+ }
|
|
|
+
|
|
|
private var productsUnavailable: Bool {
|
|
|
premiumStore.products.isEmpty && !premiumStore.isLoadingProducts
|
|
|
}
|
|
|
@@ -308,89 +415,106 @@ struct PremiumFeaturesView: View {
|
|
|
colorScheme == .dark ? .white.opacity(0.16) : .black.opacity(0.08)
|
|
|
}
|
|
|
|
|
|
- private func featureCard(title: String, subtitle: String, color: Color) -> some View {
|
|
|
- HStack(alignment: .top, spacing: 10) {
|
|
|
- Circle()
|
|
|
- .fill(color.opacity(0.95))
|
|
|
- .frame(width: 28, height: 28)
|
|
|
- .overlay(
|
|
|
- Image(systemName: "sparkles")
|
|
|
- .font(.system(size: 11, weight: .semibold))
|
|
|
- .foregroundStyle(.white)
|
|
|
- )
|
|
|
+ private func featureCard(title: String, subtitle: String, color: Color, desktop: Bool) -> some View {
|
|
|
+ HStack(alignment: .top, spacing: desktop ? 12 : 10) {
|
|
|
+ Image(systemName: "checkmark.circle.fill")
|
|
|
+ .font(.system(size: desktop ? 20 : 16, weight: .semibold))
|
|
|
+ .foregroundStyle(color)
|
|
|
+ .frame(width: desktop ? 24 : 22, alignment: .center)
|
|
|
|
|
|
- VStack(alignment: .leading, spacing: 3) {
|
|
|
+ VStack(alignment: .leading, spacing: 4) {
|
|
|
Text(title)
|
|
|
- .font(.system(size: 11.5, weight: .bold))
|
|
|
+ .font(.system(size: desktop ? 13 : 11.5, weight: .semibold))
|
|
|
.foregroundStyle(primaryTextColor)
|
|
|
- .lineLimit(1)
|
|
|
+ .lineLimit(2)
|
|
|
Text(subtitle)
|
|
|
- .font(.system(size: 9.5, weight: .medium))
|
|
|
+ .font(.system(size: desktop ? 11.5 : 9.5, weight: .medium))
|
|
|
.foregroundStyle(secondaryTextColor)
|
|
|
- .lineLimit(2)
|
|
|
+ .lineLimit(desktop ? 4 : 2)
|
|
|
}
|
|
|
Spacer(minLength: 0)
|
|
|
}
|
|
|
- .padding(10)
|
|
|
- .frame(maxWidth: .infinity, minHeight: 66, alignment: .leading)
|
|
|
+ .padding(desktop ? 12 : 10)
|
|
|
+ .frame(maxWidth: .infinity, minHeight: desktop ? 74 : 66, alignment: .leading)
|
|
|
.background(
|
|
|
- RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
|
- .fill(colorScheme == .dark ? Color.white.opacity(0.14) : Color.black.opacity(0.05))
|
|
|
+ RoundedRectangle(cornerRadius: desktop ? 12 : 12, style: .continuous)
|
|
|
+ .fill(colorScheme == .dark ? Color.white.opacity(0.08) : Color.black.opacity(0.05))
|
|
|
)
|
|
|
}
|
|
|
|
|
|
- private func priceCard(title: String, price: String, subtitle: String, badgeText: String?, isSelected: Bool) -> some View {
|
|
|
+ private func priceCard(title: String, price: String, subtitle: String, badgeText: String?, isSelected: Bool, compact: Bool) -> some View {
|
|
|
let cardStroke: Color = {
|
|
|
if isSelected { return Color.blue.opacity(0.95) }
|
|
|
- return colorScheme == .dark ? Color.white.opacity(0.4) : Color.black.opacity(0.18)
|
|
|
+ return colorScheme == .dark ? Color.white.opacity(0.35) : Color.black.opacity(0.16)
|
|
|
}()
|
|
|
let cardFill: Color = {
|
|
|
- if isSelected { return Color.blue.opacity(colorScheme == .dark ? 0.12 : 0.10) }
|
|
|
- return colorScheme == .dark ? Color.white.opacity(0.05) : Color.black.opacity(0.025)
|
|
|
+ if isSelected { return Color.blue.opacity(colorScheme == .dark ? 0.14 : 0.11) }
|
|
|
+ return colorScheme == .dark ? Color.white.opacity(0.055) : Color.black.opacity(0.03)
|
|
|
}()
|
|
|
- let cardStrokeWidth: CGFloat = isSelected ? 2 : 1.5
|
|
|
+ let cardStrokeWidth: CGFloat = isSelected ? 2.25 : 1
|
|
|
+
|
|
|
+ let titleSize: CGFloat = compact ? 13 : 15
|
|
|
+ let subtitleSize: CGFloat = compact ? 10 : 12
|
|
|
+ let priceSize: CGFloat = compact ? 15 : 18
|
|
|
+ let badgeSize: CGFloat = compact ? 9 : 10
|
|
|
+ let vPad: CGFloat = compact ? 11 : 13
|
|
|
|
|
|
return HStack(alignment: .center) {
|
|
|
- VStack(alignment: .leading, spacing: 5) {
|
|
|
+ VStack(alignment: .leading, spacing: compact ? 3 : 5) {
|
|
|
Text(title)
|
|
|
- .font(.system(size: 12, weight: .bold))
|
|
|
+ .font(.system(size: titleSize, weight: .bold))
|
|
|
.foregroundStyle(primaryTextColor)
|
|
|
Text(subtitle)
|
|
|
- .font(.system(size: 7.5, weight: .medium))
|
|
|
+ .font(.system(size: subtitleSize, weight: .medium))
|
|
|
.foregroundStyle(secondaryTextColor)
|
|
|
+ .fixedSize(horizontal: false, vertical: true)
|
|
|
}
|
|
|
|
|
|
- Spacer()
|
|
|
+ Spacer(minLength: 8)
|
|
|
|
|
|
- VStack(alignment: .trailing, spacing: 7) {
|
|
|
+ VStack(alignment: .trailing, spacing: compact ? 5 : 6) {
|
|
|
Text(price)
|
|
|
- .font(.system(size: 10, weight: .bold))
|
|
|
+ .font(.system(size: priceSize, weight: .bold))
|
|
|
.foregroundStyle(primaryTextColor)
|
|
|
- .minimumScaleFactor(0.7)
|
|
|
+ .minimumScaleFactor(0.65)
|
|
|
.lineLimit(1)
|
|
|
if let badgeText {
|
|
|
Text(badgeText)
|
|
|
- .font(.system(size: 7.5, weight: .bold))
|
|
|
+ .font(.system(size: badgeSize, weight: .bold))
|
|
|
.foregroundStyle(.white)
|
|
|
- .padding(.horizontal, 8)
|
|
|
- .padding(.vertical, 4)
|
|
|
+ .padding(.horizontal, compact ? 7 : 9)
|
|
|
+ .padding(.vertical, compact ? 3 : 5)
|
|
|
.background(
|
|
|
- RoundedRectangle(cornerRadius: 7, style: .continuous)
|
|
|
+ RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
|
.fill(Color.blue.opacity(0.95))
|
|
|
)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- .padding(10)
|
|
|
+ .padding(.horizontal, compact ? 12 : 14)
|
|
|
+ .padding(.vertical, vPad)
|
|
|
.background(
|
|
|
- RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
|
+ RoundedRectangle(cornerRadius: compact ? 12 : 14, style: .continuous)
|
|
|
.stroke(cardStroke, lineWidth: cardStrokeWidth)
|
|
|
.background(
|
|
|
- RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
|
+ RoundedRectangle(cornerRadius: compact ? 12 : 14, style: .continuous)
|
|
|
.fill(cardFill)
|
|
|
)
|
|
|
)
|
|
|
- .contentShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
|
|
+ .contentShape(RoundedRectangle(cornerRadius: compact ? 12 : 14, style: .continuous))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+enum PremiumPaywallWindowSizing {
|
|
|
+ /// Sizes the paywall to sit inside the parent window (launcher default content ~980×700, browser ~1200×800).
|
|
|
+ static func contentSize(forParentWindow parent: NSWindow) -> NSSize {
|
|
|
+ let inset: CGFloat = 48
|
|
|
+ let content = parent.contentRect(forFrameRect: parent.frame)
|
|
|
+ let usableW = content.width - inset * 2
|
|
|
+ let usableH = content.height - inset * 2
|
|
|
+ let w = min(1000, max(600, usableW))
|
|
|
+ let h = min(720, max(520, usableH))
|
|
|
+ return NSSize(width: floor(w), height: floor(h))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -398,6 +522,6 @@ struct PremiumFeaturesView: View {
|
|
|
#Preview {
|
|
|
PremiumFeaturesView()
|
|
|
.environmentObject(PremiumStore.shared)
|
|
|
- .frame(width: 560, height: 690)
|
|
|
+ .frame(width: 920, height: 620)
|
|
|
}
|
|
|
#endif
|