|
|
@@ -3,6 +3,7 @@ import SwiftUI
|
|
|
struct PaywallView: View {
|
|
|
let onClose: () -> Void
|
|
|
@StateObject private var viewModel = PaywallViewModel()
|
|
|
+ @State private var isCTAHovered = false
|
|
|
|
|
|
var body: some View {
|
|
|
GeometryReader { proxy in
|
|
|
@@ -93,7 +94,7 @@ struct PaywallView: View {
|
|
|
Spacer()
|
|
|
}
|
|
|
.padding(.vertical, 14)
|
|
|
- .background(Color(red: 0.03, green: 0.62, blue: 0.48))
|
|
|
+ .background(isCTAHovered ? Color(red: 0.02, green: 0.56, blue: 0.43) : Color(red: 0.03, green: 0.62, blue: 0.48))
|
|
|
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
|
|
.overlay(alignment: .trailing) {
|
|
|
Image(systemName: "chevron.right.2")
|
|
|
@@ -102,7 +103,15 @@ struct PaywallView: View {
|
|
|
.padding(.trailing, 24)
|
|
|
}
|
|
|
}
|
|
|
+ .scaleEffect(isCTAHovered ? 1.01 : 1.0)
|
|
|
+ .shadow(
|
|
|
+ color: isCTAHovered ? AppTheme.primaryShadow.opacity(0.32) : .clear,
|
|
|
+ radius: isCTAHovered ? 8 : 0,
|
|
|
+ y: isCTAHovered ? 3 : 0
|
|
|
+ )
|
|
|
+ .animation(.easeOut(duration: 0.15), value: isCTAHovered)
|
|
|
.buttonStyle(.plain)
|
|
|
+ .onHover { isCTAHovered = $0 }
|
|
|
|
|
|
Text("No commitment, cancel anytime.")
|
|
|
.font(.system(size: 13, weight: .semibold))
|
|
|
@@ -119,10 +128,10 @@ struct PaywallView: View {
|
|
|
.lineSpacing(2)
|
|
|
|
|
|
HStack(spacing: 0) {
|
|
|
- footerLink("Continue with free plan", action: onClose)
|
|
|
- footerLink("Privacy Policy")
|
|
|
- footerLink("Support")
|
|
|
- footerLink("Terms of Services")
|
|
|
+ PaywallFooterLinkView(title: "Continue with free plan", action: onClose)
|
|
|
+ PaywallFooterLinkView(title: "Privacy Policy")
|
|
|
+ PaywallFooterLinkView(title: "Support")
|
|
|
+ PaywallFooterLinkView(title: "Terms of Services")
|
|
|
}
|
|
|
.frame(height: 44)
|
|
|
.frame(maxWidth: .infinity)
|
|
|
@@ -131,19 +140,6 @@ struct PaywallView: View {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private func footerLink(_ title: String, action: @escaping () -> Void = {}) -> some View {
|
|
|
- Button(action: action) {
|
|
|
- Text(title)
|
|
|
- .font(.system(size: 14, weight: .semibold))
|
|
|
- .foregroundStyle(Color(red: 0.59, green: 0.62, blue: 0.68))
|
|
|
- .lineLimit(1)
|
|
|
- .minimumScaleFactor(0.75)
|
|
|
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
|
|
- .contentShape(Rectangle())
|
|
|
- }
|
|
|
- .frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
- .buttonStyle(.plain)
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
private struct PaywallFeatureBox: View {
|
|
|
@@ -195,6 +191,7 @@ private struct PaywallPlanCardView: View {
|
|
|
let plan: PaywallPlan
|
|
|
let isSelected: Bool
|
|
|
let onSelect: () -> Void
|
|
|
+ @State private var isHovered = false
|
|
|
|
|
|
var body: some View {
|
|
|
Button(action: onSelect) {
|
|
|
@@ -267,21 +264,53 @@ private struct PaywallPlanCardView: View {
|
|
|
)
|
|
|
}
|
|
|
.frame(maxWidth: .infinity, minHeight: 170, alignment: .topLeading)
|
|
|
- .background(isSelected ? AppTheme.tealLight.opacity(0.28) : AppTheme.cardBackground)
|
|
|
+ .background((isSelected || isHovered) ? AppTheme.tealLight.opacity(isSelected ? 0.28 : 0.16) : AppTheme.cardBackground)
|
|
|
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
|
|
|
.overlay(
|
|
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
|
.stroke(
|
|
|
- isSelected ? AppTheme.toolbarBorderHover : AppTheme.border,
|
|
|
+ (isSelected || isHovered) ? AppTheme.toolbarBorderHover : AppTheme.border,
|
|
|
lineWidth: isSelected ? 2 : 1
|
|
|
)
|
|
|
)
|
|
|
.shadow(
|
|
|
- color: isSelected ? AppTheme.primaryShadow.opacity(0.35) : .clear,
|
|
|
- radius: isSelected ? 6 : 0,
|
|
|
- y: isSelected ? 2 : 0
|
|
|
+ color: (isSelected || isHovered) ? AppTheme.primaryShadow.opacity(isSelected ? 0.35 : 0.22) : .clear,
|
|
|
+ radius: (isSelected || isHovered) ? (isSelected ? 6 : 4) : 0,
|
|
|
+ y: (isSelected || isHovered) ? 2 : 0
|
|
|
)
|
|
|
+ .scaleEffect(isHovered && !isSelected ? 1.008 : 1.0)
|
|
|
+ .animation(.easeOut(duration: 0.15), value: isHovered)
|
|
|
}
|
|
|
.buttonStyle(.plain)
|
|
|
+ .onHover { isHovered = $0 }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+private struct PaywallFooterLinkView: View {
|
|
|
+ let title: String
|
|
|
+ let action: () -> Void
|
|
|
+ @State private var isHovered = false
|
|
|
+
|
|
|
+ init(title: String, action: @escaping () -> Void = {}) {
|
|
|
+ self.title = title
|
|
|
+ self.action = action
|
|
|
+ }
|
|
|
+
|
|
|
+ var body: some View {
|
|
|
+ Button(action: action) {
|
|
|
+ Text(title)
|
|
|
+ .font(.system(size: 14, weight: .semibold))
|
|
|
+ .foregroundStyle(isHovered ? Color(red: 0.47, green: 0.51, blue: 0.57) : Color(red: 0.59, green: 0.62, blue: 0.68))
|
|
|
+ .lineLimit(1)
|
|
|
+ .minimumScaleFactor(0.75)
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
|
|
+ .contentShape(Rectangle())
|
|
|
+ }
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
+ .background(isHovered ? AppTheme.cardBackground : Color.clear)
|
|
|
+ .scaleEffect(isHovered ? 1.01 : 1.0)
|
|
|
+ .animation(.easeOut(duration: 0.15), value: isHovered)
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ .onHover { isHovered = $0 }
|
|
|
}
|
|
|
}
|