mirror of
https://github.com/confirmedcode/Lockdown-iOS.git
synced 2026-03-02 18:23:49 +01:00
218 lines
6.9 KiB
Swift
218 lines
6.9 KiB
Swift
//
|
|
// OnboardingView.swift
|
|
// LockdowniOS
|
|
//
|
|
// Created by George Apostu on 13/2/25.
|
|
// Copyright © 2025 Confirmed Inc. All rights reserved.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct OnboardingView: View {
|
|
|
|
@StateObject var paywallModel: OneTimePaywallModel
|
|
|
|
@State var selectedTab: Int = {
|
|
if #available(iOS 17, *) {
|
|
2
|
|
} else {
|
|
0
|
|
}
|
|
}()
|
|
|
|
func selectNextTab() {
|
|
guard let step = OnboardingStep(rawValue: selectedTab) else { return }
|
|
if step == .first {
|
|
selectedTab = OnboardingStep.second.rawValue
|
|
ReviewAlertManager.showOnboardingAlert()
|
|
} else if step == .second {
|
|
selectedTab = OnboardingStep.paywall.rawValue
|
|
}
|
|
}
|
|
|
|
let steps: [OnboardingStep] = [.first, .second]
|
|
|
|
var body: some View {
|
|
TabView(selection: $selectedTab) {
|
|
Group {
|
|
ForEach(steps) { step in
|
|
OnboardingStepView(step: step) {
|
|
withAnimation {
|
|
selectNextTab()
|
|
}
|
|
}
|
|
.tag(step.rawValue)
|
|
}
|
|
OneTimePaywallView(model: paywallModel)
|
|
.tag(OnboardingStep.paywall.rawValue)
|
|
}
|
|
.onAppear {
|
|
selectedTab = 1
|
|
selectedTab = 0
|
|
}
|
|
}
|
|
.tabViewStyle(.page(indexDisplayMode: .never))
|
|
.animation(.easeInOut, value: selectedTab)
|
|
.onAppear {
|
|
UIScrollView.appearance().bounces = false
|
|
UIScrollView.appearance().isScrollEnabled = false
|
|
}
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
OnboardingView(paywallModel: OneTimePaywallModel(products: VPNSubscription.oneTimeProducts, infos: [.mockWeekly, .mockWeeklyTrial, .mockYearly, .mockWeeklyTrial]))
|
|
}
|
|
|
|
struct OnboardingStepView: View {
|
|
|
|
let step: OnboardingStep
|
|
|
|
@State private var arrowOffset: CGFloat = -3.125
|
|
|
|
var continueAction: () -> Void = { }
|
|
|
|
var body: some View {
|
|
ZStack(alignment: .bottom) {
|
|
Image(step.backgroundImageName)
|
|
.resizable()
|
|
.scaledToFill()
|
|
.ignoresSafeArea()
|
|
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height)
|
|
|
|
VStack(alignment: .leading, spacing: 25.0) {
|
|
Text(step.title)
|
|
.foregroundColor(.white)
|
|
.font(.system(size: 28, weight: .semibold))
|
|
.frame(maxWidth: .infinity)
|
|
.multilineTextAlignment(.center)
|
|
|
|
Text(step.subtitle)
|
|
.foregroundColor(.white)
|
|
.frame(maxWidth: .infinity)
|
|
.multilineTextAlignment(.center)
|
|
.font(.custom("SFPro", size: 20))
|
|
.padding(.horizontal, 25)
|
|
.padding(.bottom, 25)
|
|
|
|
VStack(alignment: .leading, spacing: 25) {
|
|
ForEach(step.items, id: \.self) { item in
|
|
HStack(spacing: 12) {
|
|
Image("onboardingCheckmark")
|
|
Text(item)
|
|
.lineLimit(nil)
|
|
.foregroundColor(.white)
|
|
}
|
|
}
|
|
}
|
|
.font(.custom("SFPro", size: 14))
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.horizontal, 10)
|
|
|
|
|
|
Button(action: {
|
|
continueAction()
|
|
}, label: {
|
|
ZStack(alignment: .trailing) {
|
|
Text("Onboarding.Continue")
|
|
.font(.custom("Montserrat-SemiBold", size: 20))
|
|
.foregroundColor(.white)
|
|
.padding()
|
|
.padding(.vertical, 3)
|
|
.frame(maxWidth: .infinity)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 50)
|
|
.fill(Color("Confirmed Blue"))
|
|
)
|
|
Image(systemName: "arrow.right")
|
|
.foregroundColor(.white)
|
|
.padding(20)
|
|
.offset(x: arrowOffset)
|
|
.animation(Animation.easeInOut(duration: 0.39).repeatForever(autoreverses: true), value: arrowOffset)
|
|
.onAppear {
|
|
arrowOffset = 3.125
|
|
}
|
|
}
|
|
})
|
|
.padding(.bottom, 30)
|
|
}
|
|
.padding(40)
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
struct OnboardingPage: Identifiable {
|
|
let id = UUID()
|
|
let image: String
|
|
let title: String
|
|
let subtitle: String
|
|
var isFirst: Bool = false
|
|
var isLast: Bool = false
|
|
}
|
|
|
|
enum OnboardingStep: Int {
|
|
|
|
case first = 0
|
|
case second = 1
|
|
case paywall = 2
|
|
|
|
var backgroundImageName: String {
|
|
// step1 & step2 are reversed by design
|
|
switch self {
|
|
case .first:
|
|
return "onboardingStep2"
|
|
case .second:
|
|
return "onboardingStep1"
|
|
case .paywall:
|
|
return "onboardingPaywall"
|
|
}
|
|
}
|
|
|
|
var title: String {
|
|
// step1 & step2 are reversed by design
|
|
switch self {
|
|
case .first:
|
|
return NSLocalizedString("Onboarding.Step2.Title", comment: "")
|
|
case .second:
|
|
return NSLocalizedString("Onboarding.Step1.Title", comment: "")
|
|
case .paywall:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
var subtitle: String {
|
|
// step1 & step2 are reversed by design
|
|
switch self {
|
|
case .first:
|
|
return NSLocalizedString("Onboarding.Step2.Subtitle", comment: "")
|
|
case .second:
|
|
return NSLocalizedString("Onboarding.Step1.Subtitle", comment: "")
|
|
case .paywall:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
var items: [String] {
|
|
// step1 & step2 are reversed by design
|
|
switch self {
|
|
case .first:
|
|
return [NSLocalizedString("Onboarding.Step2.Item1", comment: ""),
|
|
NSLocalizedString("Onboarding.Step2.Item2", comment: ""),
|
|
NSLocalizedString("Onboarding.Step2.Item3", comment: "")]
|
|
case .second:
|
|
return [NSLocalizedString("Onboarding.Step1.Item1", comment: ""),
|
|
NSLocalizedString("Onboarding.Step1.Item2", comment: ""),
|
|
NSLocalizedString("Onboarding.Step1.Item3", comment: "")]
|
|
case .paywall:
|
|
return []
|
|
}
|
|
}
|
|
}
|
|
|
|
extension OnboardingStep: Identifiable {
|
|
var id: RawValue { rawValue }
|
|
}
|