🚀 Start Project

This commit is contained in:
Victor Bodinaud
2024-10-14 17:02:14 +02:00
parent 7850943a32
commit e9719414e9
53 changed files with 985 additions and 443 deletions

View File

@@ -10,9 +10,22 @@
57282ACB2CBD2810000C443E /* AlloVoisinsSwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AlloVoisinsSwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
57282BA72CBD579A000C443E /* Exceptions for "AlloVoisinsSwiftUI" folder in "AlloVoisinsSwiftUI" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 57282ACA2CBD2810000C443E /* AlloVoisinsSwiftUI */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
57282ACD2CBD2810000C443E /* AlloVoisinsSwiftUI */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
57282BA72CBD579A000C443E /* Exceptions for "AlloVoisinsSwiftUI" folder in "AlloVoisinsSwiftUI" target */,
);
path = AlloVoisinsSwiftUI;
sourceTree = "<group>";
};
@@ -255,6 +268,7 @@
DEVELOPMENT_TEAM = WVH3Y23X7X;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = AlloVoisinsSwiftUI/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -284,6 +298,7 @@
DEVELOPMENT_TEAM = WVH3Y23X7X;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = AlloVoisinsSwiftUI/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "9F0CD4E1-EA25-475C-9981-B5E4AE8BD8B9"
type = "1"
version = "2.0">
</Bucket>

View File

@@ -11,7 +11,9 @@ import SwiftUI
struct AlloVoisinsSwiftUIApp: App {
var body: some Scene {
WindowGroup {
ContentView()
NavigationStack {
ResiliationNavigationView(resiliationType: .apProWithTrial)
}
}
}
}

View File

@@ -0,0 +1,159 @@
//
// ResponsiveSheetWrapper.swift
//
// Created by Bacem Ben Afia on 04/08/2022.
// ba.bessem@gmail.com
import SwiftUI
import UIKit
//MARK: - ViewModifier
fileprivate struct ResponsiveSheetWrapper: ViewModifier {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
func body(content: Content) -> some View {
/// check device type (ipad sheet are centred / iPhone sheet pinned to bottom )
if UIDevice.current.userInterfaceIdiom == .phone {
ZStack (alignment: .bottom){
Color(UIColor.systemBackground.withAlphaComponent(0.01))
.onTapGesture {
// tap outside the view to dismiss
presentationMode.wrappedValue.dismiss()
}
.edgesIgnoringSafeArea(.all)
VStack {
/// the small thumb for bottom sheet native like
content
}
/// redesign the content
.asResponsiveSheetContent()
}
/// remove system background
.clearSheetSystemBackground()
.edgesIgnoringSafeArea(.bottom)
} else {
ZStack {
Color(UIColor.systemBackground.withAlphaComponent(0.01))
.onTapGesture {
// tap outside the view to dismiss
presentationMode.wrappedValue.dismiss()
}
content
/// redesign the content
.asResponsiveSheetContent()
}
/// remove system background
.clearSheetSystemBackground()
.edgesIgnoringSafeArea(.all)
}
}
}
fileprivate struct ResponsiveSheetContent: ViewModifier {
/// ResponsiveSheetContent will create the form of a bottom sheet (apply corners radius for both iPad an iPhone sheet)
@Environment(\.safeAreaInsets) private var safeAreaInsets
func body(content: Content) -> some View {
if UIDevice.current.userInterfaceIdiom == .phone {
content
.padding(.bottom, safeAreaInsets.bottom)
.background(Color(UIColor.systemBackground))
.cornerRadius(10, corners: [.topLeft, .topRight])
} else {
content
.padding()
.background(Color(UIColor.systemBackground))
.cornerRadius(10, corners: [.allCorners])
}
}
}
fileprivate struct ClearBackgroundViewModifier: ViewModifier {
func body(content: Content) -> some View {
content
.background(ClearBackgroundView())
}
}
fileprivate struct CornerRadiusStyle: ViewModifier {
var radius: CGFloat
var corners: UIRectCorner
struct CornerRadiusShape: Shape {
var radius = CGFloat.infinity
var corners = UIRectCorner.allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}
func body(content: Content) -> some View {
content
.clipShape(CornerRadiusShape(radius: radius, corners: corners))
}
}
//MARK: - UIViewRepresentable
fileprivate struct ClearBackgroundView: UIViewRepresentable {
/// The Key
func makeUIView(context: Context) -> UIView {
let view = UIView()
DispatchQueue.main.async {
/// GOD BLESS UI KIT
/// Target sheet system background view
/// Apply clear Color
view.superview?.superview?.backgroundColor = .clear
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
//MARK: - View Extension
extension View {
/// Return a formatted View Based on Device Type
/// if iPad => Centred Alert-Like View
/// if iPhone => Bottom Sheet View
/// That's all folks
func asResponsiveSheet() -> some View {
self.modifier(ResponsiveSheetWrapper())
}
fileprivate func asResponsiveSheetContent() -> some View {
self.modifier(ResponsiveSheetContent())
}
fileprivate func clearSheetSystemBackground() -> some View {
self.modifier(ClearBackgroundViewModifier())
}
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
self.modifier(CornerRadiusStyle(radius: radius, corners: corners))
}
}
//MARK: - EnvironmentKey & Values
fileprivate struct SafeAreaInsetsKey: EnvironmentKey {
static var defaultValue: EdgeInsets {
(UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.safeAreaInsets ?? .zero).insets
}
}
fileprivate extension EnvironmentValues {
var safeAreaInsets: EdgeInsets {
self[SafeAreaInsetsKey.self]
}
}
//MARK: - UIEdgeInsets Extension
fileprivate extension UIEdgeInsets {
var insets: EdgeInsets {
EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
}
}

View File

@@ -0,0 +1,36 @@
//
// FontRegistration.swift
//
//
// Created by Victor on 12/06/2024.
//
import CoreGraphics
import CoreText
import UIKit
struct FontRegistration {
public enum FontError: Swift.Error {
case failedToRegisterFont
}
func registerFonts() throws {
let fontNames = [
"TTChocolates-Bold-Italic",
"TTChocolates-Bold",
"TTChocolates-DemiBold",
"TTChocolates-Medium",
"TTChocolates-MediumIt"
]
for name in fontNames {
guard let asset = NSDataAsset(name: "Fonts/\(name)"),
let provider = CGDataProvider(data: asset.data as NSData),
let font = CGFont(provider),
CTFontManagerRegisterGraphicsFont(font, nil)
else {
throw FontError.failedToRegisterFont
}
}
}
}

View File

@@ -0,0 +1,55 @@
//
// Extension+Color.swift
//
//
// Created by Victor on 12/06/2024.
//
import SwiftUI
extension Color {
static func sqRoyal(_ variant: Int = 60) -> Color {
return Color("ROYAL_\(variant)")
}
static func sqPurple(_ variant: Int = 60) -> Color {
return Color("PURPLE_\(variant)")
}
static func sqNeutral(_ variant: Int = 60) -> Color {
return Color("NEUTRAL_\(variant)")
}
static func sqOrange(_ variant: Int = 60) -> Color {
return Color("ORANGE_\(variant)")
}
static func sqGreen(_ variant: Int = 60) -> Color {
return Color("GREEN_\(variant)")
}
static func sqYellow(_ variant: Int = 60) -> Color {
return Color("YELLOW_\(variant)")
}
static func sqGold(_ variant: Int = 60) -> Color {
return Color("GOLD_\(variant)")
}
static func sqBlue(_ variant: Int = 60) -> Color {
return Color("BLUE_\(variant)")
}
static func sqGrape(_ variant: Int = 60) -> Color {
return Color("GRAPE_\(variant)")
}
static let sqSemanticRed = Color("SEMANTIC_RED")
static let sqSemanticOrange = Color("SEMANTIC_CRITICAL")
static let sqSemanticGreen = Color("SEMANTIC_GREEN")
static let sqSemanticBlue = Color("SEMANTIC_BLUE")
static let sqSemanticCritical = Color("SEMANTIC_CRITICAL")
static let sqSemanticWarning = Color("SEMANTIC_WARNING")
static let sqSemanticPositive = Color("SEMANTIC_GREEN")
static let sqSemanticNegative = Color("SEMANTIC_RED")
}

View File

@@ -0,0 +1,41 @@
//
// Extension+View.swift
//
//
// Created by Victor on 19/06/2024.
//
import SwiftUI
//extension View {
// func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
// clipShape( RoundedCorner(radius: radius, corners: corners) )
// }
//}
//
//struct RoundedCorner: Shape {
// let radius: CGFloat
// let corners: UIRectCorner
//
// init(radius: CGFloat = .infinity, corners: UIRectCorner = .allCorners) {
// self.radius = radius
// self.corners = corners
// }
//
// func path(in rect: CGRect) -> Path {
// let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
// return Path(path.cgPath)
// }
//}
extension View {
func sqFont(_ font: SQTextFont, size: CGFloat = 16) -> some View {
self.font(.custom(font.rawValue, size: size))
}
}
extension Font {
static func sq(_ font: SQTextFont, size: CGFloat = 16) -> Font {
.custom(font.rawValue, size: size)
}
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIAppFonts</key>
<array>
<string>TTChocolates-Bold.otf</string>
<string>TTChocolates-Bold-Italic.otf</string>
<string>TTChocolates-DemiBold.otf</string>
<string>TTChocolates-Medium.otf</string>
<string>TTChocolates-MediumIt.otf</string>
</array>
</dict>
</plist>

View File

@@ -8,13 +8,6 @@
import SwiftUI
struct BoosterConfirmationScreen: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View {
ZStack {
@@ -22,7 +15,7 @@ struct BoosterConfirmationScreen: View {
.ignoresSafeArea()
VStack {
VStack(spacing: 32) {
Image("booster_logo", bundle: Bundle.module)
Image("booster_logo")
.resizable()
.frame(width: 210, height: 180)
SQText("Cest confirmé !", size: 18, font: .bold)

View File

@@ -8,13 +8,6 @@
import SwiftUI
struct BoosterKnowAboutScreen: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View {
ScrollView(showsIndicators: false) {

View File

@@ -8,11 +8,6 @@
import SwiftUI
struct BoosterSubscriptionManagementScreen: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {}
}
@State var selectedValue = 0
@State var presentControls = false
@@ -43,9 +38,9 @@ struct BoosterSubscriptionManagementScreen: View {
}
}
}
.bottomSheet(isShowing: $presentControls, content: {
BoosterSubscriptionOptionsView()
})
// .bottomSheet(isShowing: $presentControls, content: {
// BoosterSubscriptionOptionsView()
// })
}
}

View File

@@ -40,12 +40,6 @@ struct BoosterSubscriptionSelectionScreen: View {
@State var mode: BoosterSubscriptionMode = .edit
@State var isNotPremier = false
init() {
do {
try FontRegistration().registerFonts()
} catch {}
}
var body: some View {
NavigationView {
ZStack(alignment: .top) {
@@ -63,7 +57,7 @@ struct BoosterSubscriptionSelectionScreen: View {
VStack(alignment: .trailing) {
BoosterLockedToPremierView()
VStack(spacing: 16) {
Image("booster_logo", bundle: Bundle.module)
Image("booster_logo")
.resizable()
.frame(width: 210, height: 180)

View File

@@ -8,11 +8,6 @@
import SwiftUI
struct OnlyForPremierModal: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {}
}
var body: some View {
NavigationView {
@@ -23,7 +18,7 @@ struct OnlyForPremierModal: View {
.fill(Color.sqOrange(50))
.frame(height: 200)
VStack(alignment: .leading, spacing: 32) {
Image("only_for_premier", bundle: .module)
Image("only_for_premier")
.resizable()
.frame(width: 307, height: 108)
}

View File

@@ -8,11 +8,6 @@
import SwiftUI
struct RegulatedProfessionEditProfilModal: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {}
}
var body: some View {
VStack {

View File

@@ -8,11 +8,6 @@
import SwiftUI
struct RegulatedProfessionModal: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {}
}
var body: some View {
VStack() {

View File

@@ -8,86 +8,84 @@
import SwiftUI
struct AlloVoisinReputationScreen: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {}
}
var body: some View {
VStack(spacing: 32) {
VStack {
SQText("Le saviez-vous ?", size: 20, font: .bold)
SQText("AlloVoisins en France, cest :", size: 20, font: .bold)
}
VStack {
SQText("4,5 millions", size: 32, font: .bold)
SQText("de membres, partout en France")
}
.padding(16)
.background(Color.white)
.cornerRadius(8)
VStack(spacing: 8) {
VStack(spacing: 16) {
VStack(spacing: 32) {
VStack {
HStack {
SQIcon(.star, size: .xxl, type: .solid, color: .sqGold(50))
SQText("4,6/5", size: 32, font: .bold)
}
SQText("Calculé à partir de 107,1 k avis")
SQText("Le saviez-vous ?", size: 20, font: .bold)
SQText("AlloVoisins en France, cest :", size: 20, font: .bold)
}
HStack(spacing: 16) {
HStack(spacing: 2) {
SQIcon(.apple_brand, size: .s)
SQText("App Store", size: 10)
VStack {
SQText("4,5 millions", size: 32, font: .bold)
SQText("de membres, partout en France")
}
.padding(16)
.background(Color.white)
.cornerRadius(8)
VStack(spacing: 8) {
VStack {
HStack {
SQIcon(.star, size: .xxl, type: .solid, color: .sqGold(50))
SQText("4,6/5", size: 32, font: .bold)
}
SQText("Calculé à partir de 107,1 k avis")
}
HStack(spacing: 2) {
SQIcon(.play_store_brand, size: .s)
SQText("Google Play", size: 10)
HStack(spacing: 16) {
HStack(spacing: 2) {
SQIcon(.apple_brand, size: .s)
SQText("App Store", size: 10)
}
HStack(spacing: 2) {
SQIcon(.play_store_brand, size: .s)
SQText("Google Play", size: 10)
}
HStack(spacing: 2) {
SQIcon(.star_trustpilot_brand, size: .s)
SQText("Trustpilot", size: 10)
}
}
HStack(spacing: 2) {
SQIcon(.star_trustpilot_brand, size: .s)
SQText("Trustpilot", size: 10)
}
.padding(16)
.background(Color.white)
.cornerRadius(8)
VStack {
SQText("La presse en parle", size: 24, font: .bold)
HStack(spacing: 16) {
Image("m6")
.resizable()
.scaledToFit()
.frame(height: 24)
Image("rtl")
.resizable()
.scaledToFit()
.frame(height: 24)
Image("tf1")
.resizable()
.scaledToFit()
.frame(height: 24)
Image("bfm")
.resizable()
.scaledToFit()
.frame(height: 24)
}
}
.padding(16)
.background(Color.white)
.cornerRadius(8)
SQButton("Conserver mon abonnement", color: .sqNeutral(100), textColor: .white) {
}
}
.padding(16)
.background(Color.white)
.background(Color.sqBlue(10))
.cornerRadius(8)
VStack {
SQText("La presse en parle", size: 24, font: .bold)
HStack(spacing: 16) {
Image("m6", bundle: Bundle.module)
.resizable()
.scaledToFit()
.frame(height: 24)
Image("rtl", bundle: Bundle.module)
.resizable()
.scaledToFit()
.frame(height: 24)
Image("tf1", bundle: Bundle.module)
.resizable()
.scaledToFit()
.frame(height: 24)
Image("bfm", bundle: Bundle.module)
.resizable()
.scaledToFit()
.frame(height: 24)
}
}
.padding(16)
.background(Color.white)
.cornerRadius(8)
SQButton("Conserver mon abonnement", color: .sqNeutral(100), textColor: .white) {
SQButton("Jai compris, mais je souhaite résilier", color: .white, textSize: 13) {
}
}
.padding(16)
.background(Color.sqBlue(10))
.cornerRadius(8)
SQButton("Jai compris, mais je souhaite résilier", color: .white, textSize: 13) {
}
.sqNavigationBar(title: "Ne partez pas !")
}
}

View File

@@ -23,6 +23,7 @@ struct ContinueAsParticularScreen: View {
}
}
}
.sqNavigationBar(title: "Ne partez pas !")
.padding(16)
}
}

View File

@@ -8,13 +8,6 @@
import SwiftUI
struct GetMoreRatingsScreen: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View {
VStack(spacing: 16) {
@@ -47,6 +40,7 @@ struct GetMoreRatingsScreen: View {
}
}
.sqNavigationBar(title: "Ne partez pas !")
.padding()
}
}

View File

@@ -8,13 +8,6 @@
import SwiftUI
struct MoreTimeScreen: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View {
VStack(spacing: 16) {
@@ -39,6 +32,7 @@ struct MoreTimeScreen: View {
}
}
.sqNavigationBar(title: "Ne partez pas !")
.padding()
}
}

View File

@@ -8,20 +8,13 @@
import SwiftUI
struct OnlyProRequestsScreen: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View {
VStack(spacing: 16) {
VStack(spacing: 32) {
SQText("Demandes réservées aux pros", size: 20, font: .bold)
.multilineTextAlignment(.center)
Image("only_for_pro", bundle: Bundle.module)
Image("only_for_pro")
.resizable()
.scaledToFit()
.frame(height: 60)
@@ -47,6 +40,7 @@ struct OnlyProRequestsScreen: View {
}
}
.sqNavigationBar(title: "Ne partez pas !")
.padding()
}
}

View File

@@ -8,20 +8,13 @@
import SwiftUI
struct PersonalizedSupportScreen: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View {
VStack(spacing: 16) {
VStack(spacing: 32) {
SQText("Bénéficiez dun accompagnement personnalisé.", size: 20, font: .bold)
.multilineTextAlignment(.center)
Image("assistance", bundle: Bundle.module)
Image("assistance")
.resizable()
.scaledToFit()
.frame(height: 100)
@@ -42,6 +35,7 @@ struct PersonalizedSupportScreen: View {
}
}
.sqNavigationBar(title: "Ne partez pas !")
.padding()
}
}

View File

@@ -63,6 +63,7 @@ struct ProfileCompletionScreen: View {
}
}
.sqNavigationBar(title: "Ne partez pas !")
.padding()
}
}

View File

@@ -1,5 +1,5 @@
//
// ResiliationCheckStepView.swift
// ResiliationCheckStepsScreen.swift
// Sequoia
//
// Created by Victor on 10/10/2024.
@@ -7,58 +7,21 @@
import SwiftUI
private struct ResiliationCheckStepView: View {
var checkStep: ResiliationCheckStep
@Binding var isSelected: Bool
var body: some View {
VStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 4, style: .continuous)
.fill(isSelected ? Color.sqNeutral(100) : .clear)
.stroke(isSelected ? Color.sqNeutral(100) : Color.sqNeutral(30), lineWidth: 1)
.frame(width: 20, height: 20)
HStack(alignment: .center) {
Spacer()
Image(checkStep.image, bundle: Bundle.main)
.resizable()
.scaledToFit()
.frame(height: 100)
Spacer()
}
HStack {
SQText(checkStep.text)
.multilineTextAlignment(.leading)
Spacer()
}
.frame(maxWidth: .infinity)
}
.padding()
.frame(maxWidth: .infinity)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.sqNeutral(30), lineWidth: 1)
)
.onTapGesture {
self.isSelected.toggle()
}
}
}
struct ResiliationCheckStepsScreen: View {
@State var nextStep: Bool = false
@StateObject var viewModel: ResiliationViewModel
var steps: [ResiliationCheckStep] {
viewModel.resiliationCheckSteps
}
@State private var selectedSteps: Set<ResiliationCheckStep> = []
var allStepsSelected: Bool {
selectedSteps.count == steps.count
selectedSteps.count == viewModel.resiliationCheckSteps.count
}
var body: some View {
ScrollView {
VStack(spacing: 16) {
ForEach(steps, id: \.self) { step in
SQText("En résiliant votre abonnement vous renoncez à :", size: 18, font: .bold)
.multilineTextAlignment(.center)
ForEach(viewModel.resiliationCheckSteps, id: \.self) { step in
ResiliationCheckStepView(
checkStep: step,
isSelected: Binding(
@@ -73,9 +36,28 @@ struct ResiliationCheckStepsScreen: View {
)
)
}
if viewModel.resiliationType == .apProWithTrial || viewModel.resiliationType == .apProWithTrialWithBooster {
TrialWarningCellView()
}
HStack {
SQButton("Annuler", color: .sqNeutral(100), isFull: false) {
}
SQButton("Continuer", color: .sqNeutral(100), textColor: .white) {
nextStep.toggle()
}
.disabled(!allStepsSelected)
}
.padding()
}
.padding()
}
.navigationDestination(isPresented: $nextStep) {
ResiliationReasonScreen(viewModel: viewModel)
}
.sqNavigationBar(title: "Demander la résiliation")
}
}

View File

@@ -1,5 +1,5 @@
//
// ResiliationProcess.swift
// ResiliationNavigationView.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 14/10/2024.
@@ -32,57 +32,16 @@
import SwiftUI
struct ResiliationNavigationView: View {
@StateObject private var coordinator = ResiliationNavigationCoordinator()
@StateObject private var viewModel: ResiliationViewModel
init(resiliationType: ResiliationType) {
let coordinator = ResiliationNavigationCoordinator()
self._coordinator = StateObject(wrappedValue: coordinator)
self._viewModel = StateObject(wrappedValue: ResiliationViewModel(resiliationType: resiliationType))
}
var body: some View {
NavigationStack(path: $coordinator.path) {
NavigationStack {
ResiliationCheckStepsScreen(viewModel: viewModel)
.navigationDestination(for: ResiliationNavigationCoordinator.Screen.self) { screen in
switch screen {
case .reason:
ResiliationReasonScreen(viewModel: viewModel)
case .promotional(let promotionalScreen):
getPromotionalScreen(for: promotionalScreen)
case .final:
EmptyView()
case .checkSteps:
EmptyView()
}
}
}
.environmentObject(coordinator)
}
@ViewBuilder
private func getPromotionalScreen(for screen: ResiliationPromotionalScreen) -> some View {
switch screen {
case .webinarPresentation:
WebinaireScreen()
case .profileCompletion:
ProfileCompletionScreen()
case .editPerimeter:
ResizePerimeterScreen()
case .onlyProSearches:
OnlyProRequestsScreen()
case .oneMonthBoosterOffered:
BoosterSubscriptionSelectionScreen()
case .allovoisinsPromotion:
AlloVoisinReputationScreen()
case .extendMyTrialPeriod:
MoreTimeScreen()
case .askPartProStatusChangeWithCancellation, .askPartProStatusChange:
StatusChangeScreen()
case .customerSupportForPro, .customerSupportForPart:
PersonalizedSupportScreen()
default:
EmptyView()
.navigationBarTitle("Résiliation", displayMode: .inline)
}
}
}

View File

@@ -8,42 +8,42 @@
import SwiftUI
struct ResiliationReasonScreen: View {
@State var selectedIndex: Int? = nil
@State var resiliationOtherMotif: String = ""
@Environment(\.dismiss) var dismiss
@ObservedObject var viewModel: ResiliationViewModel
@State private var selectedIndex: Int? = nil
@State private var resiliationOtherMotif: String = ""
var body: some View {
VStack(spacing: 16) {
SQRadio(title: "Aidez-nous à nous améliorer : précisez le motif de votre résiliation",
options: [
"Je ne suis pas un professionnel, cest une erreur",
"Je reçois des demandes mais je nai pas assez de réponses positives à mes propositions",
"Je ne reçois pas assez de demandes",
"Je nai pas eu suffisamment de temps pour me faire une opinion",
"Jai des demandes, mais elles ne mintéressent pas",
"Je nai pas compris le fonctionnement de labonnement Premier",
"Je suis sur dautres plateformes plus intéressantes",
"Autre",
],
selectedIndex: $selectedIndex)
if selectedIndex == 7 {
ScrollView {
VStack(spacing: 16) {
SQRadio(title: "Aidez-nous à nous améliorer : précisez le motif de votre résiliation",
options: viewModel.resiliationReasons.map { $0.text },
selectedIndex: $selectedIndex)
if selectedIndex == viewModel.resiliationReasons.count - 1 {
SQTextField("", placeholder: "Précisez-nous le motif de votre résiliation", text: $resiliationOtherMotif)
}
HStack {
SQButton("Annuler", color: .sqNeutral(100), isFull: false) {
HStack {
SQButton("Annuler", color: .sqNeutral(100), isFull: false) {
dismiss()
}
SQButton("Continuer", color: .sqNeutral(100), textColor: .white) {
// TODO: Aller au prochain promotional screen
if let index = selectedIndex {
let selectedReason = viewModel.resiliationReasons[index]
viewModel.setSelectedReason(selectedReason)
}
}
.disabled(selectedIndex == nil)
}
SQButton("Continuer", color: .sqNeutral(100), textColor: .white) {
}
.disabled(selectedIndex == nil)
Spacer()
}
Spacer()
}
.padding()
.sqNavigationBar(title: "Demander la résiliation")
}
}
#Preview {
ResiliationReasonScreen()
}
#Preview {
ResiliationReasonScreen(viewModel: ResiliationViewModel(resiliationType: .apProWithTrial))
}

View File

@@ -9,29 +9,32 @@ import SwiftUI
struct ResizePerimeterScreen: View {
var body: some View {
VStack(spacing: 32) {
SQText("Augmentez votre périmètre afin de recevoir plus de demandes", size: 20, font: .bold)
.multilineTextAlignment(.center)
Image("perimeter", bundle: Bundle.module)
.resizable()
.scaledToFit()
.frame(height: 140)
VStack {
SQText("Le saviez-vous ?", font: .demiBold)
SQText("Si vous le souhaitez, vous pouvez modifier à tout moment votre abonnement en élargissant votre périmètre dintervention.")
VStack(spacing: 16) {
VStack(spacing: 32) {
SQText("Augmentez votre périmètre afin de recevoir plus de demandes", size: 20, font: .bold)
.multilineTextAlignment(.center)
Image("perimeter")
.resizable()
.scaledToFit()
.frame(height: 140)
VStack {
SQText("Le saviez-vous ?", font: .demiBold)
SQText("Si vous le souhaitez, vous pouvez modifier à tout moment votre abonnement en élargissant votre périmètre dintervention.")
.multilineTextAlignment(.center)
}
SQButton("Modifier mon périmètre", color: .sqOrange(50), textColor: .white) {
}
}
SQButton("Modifier mon périmètre", color: .sqOrange(50), textColor: .white) {
.padding(16)
.background(Color.sqOrange(10))
.cornerRadius(8)
SQButton("Jai compris, mais je souhaite résilier", color: .white, textSize: 13) {
}
}
.padding(16)
.background(Color.sqOrange(10))
.cornerRadius(8)
SQButton("Jai compris, mais je souhaite résilier", color: .white, textSize: 13) {
}
.sqNavigationBar(title: "Ne partez pas !")
}
}

View File

@@ -44,6 +44,7 @@ struct SoftwarePresentationScreen: View {
}
}
}
.sqNavigationBar(title: "Ne partez pas !")
.padding()
}
}

View File

@@ -8,11 +8,6 @@
import SwiftUI
struct StatusChangeScreen: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {}
}
var body: some View {
VStack {
@@ -30,6 +25,7 @@ struct StatusChangeScreen: View {
.background(Color.sqOrange(10))
.cornerRadius(8)
}
.sqNavigationBar(title: "Changer de statut")
.padding()
}
}

View File

@@ -8,20 +8,13 @@
import SwiftUI
struct WebinaireScreen: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View {
VStack(spacing: 16) {
VStack(spacing: 32) {
SQText("Webinaire spécial Pro", size: 20, font: .bold)
.multilineTextAlignment(.center)
Image("webinaire", bundle: Bundle.module)
Image("webinaire")
.resizable()
.scaledToFit()
.frame(height: 140)
@@ -43,6 +36,7 @@ struct WebinaireScreen: View {
}
}
.sqNavigationBar(title: "Ne partez pas !")
}
}

View File

@@ -5,4 +5,4 @@
// Created by Victor on 14/10/2024.
//
import Foundation
import SwiftUI

View File

@@ -6,3 +6,150 @@
//
import Foundation
import SwiftUICore
class ResiliationViewModel: ObservableObject {
@Published var resiliationType: ResiliationType
@Published var resiliationCheckSteps: [ResiliationCheckStep]
@Published var resiliationReasons: [ResiliationReason]
@Published var selectedReason: ResiliationReason?
@Published var promotionalScreens: [ResiliationPromotionalScreen] = []
@Published var currentScreen: Screen = .checkSteps
enum Screen: Equatable {
case checkSteps
case reason
case promotional(ResiliationPromotionalScreen)
case final
}
init(resiliationType: ResiliationType) {
self.resiliationType = resiliationType
self.resiliationCheckSteps = ResiliationCheckStep.getCheckSteps(for: resiliationType)
self.resiliationReasons = ResiliationReason.getRadioResiliation(for: resiliationType)
}
func moveToReason() {
currentScreen = .reason
}
func setSelectedReason(_ reason: ResiliationReason) {
selectedReason = reason
promotionalScreens = selectResiliationPromotions(
availablePromotions: getAvailablePromotionalScreens(for: reason),
eligibleScreens: getEligiblePromotionalScreens()
)
if promotionalScreens.isEmpty {
currentScreen = .final
} else {
currentScreen = .promotional(promotionalScreens.removeFirst())
}
}
func moveToNextPromotionalScreen() {
if !promotionalScreens.isEmpty {
currentScreen = .promotional(promotionalScreens.removeFirst())
} else {
currentScreen = .final
}
}
private func selectResiliationPromotions(availablePromotions: [ResiliationPromotionalScreen], eligibleScreens: [ResiliationPromotionalScreen]) -> [ResiliationPromotionalScreen] {
let shuffledArray1 = availablePromotions.shuffled()
let commonPromotions = shuffledArray1.filter { promotion in
eligibleScreens.contains(promotion)
}
let result = commonPromotions.map { $0 }
return Array(result.prefix(2))
}
private func getAvailablePromotionalScreens(for reason: ResiliationReason) -> [ResiliationPromotionalScreen] {
switch reason {
case .noPositiveResponses:
return [.externalReview, .webinarPresentation, .profileCompletion]
case .uninterestingRequests:
return [.webinarPresentation, .editPerimeter, .onlyProSearches]
case .lowDemand:
return [.externalReview, .profileCompletion, .editPerimeter, .oneMonthBoosterOffered]
case .betterPlatforms:
// TODO: Rajouter présentation Logiciel
return [.oneMonthBoosterOffered, .allovoisinsPromotion, .extendMyTrialPeriod, .webinarPresentation]
case .notPro:
return [.askPartProStatusChange]
case .insufficientTime:
return [.extendMyTrialPeriod]
case .misunderstoodPremier:
// TODO: Check si c'est le bon .cusrommerSupport*
return [.webinarPresentation, .customerSupportForPro]
default:
return [.allovoisinsPromotion]
}
}
private func getEligiblePromotionalScreens() -> [ResiliationPromotionalScreen] {
return [.extendMyTrialPeriod, .profileCompletion, .allovoisinsPromotion]
}
private func getPromotionalScreen(for screen: ResiliationPromotionalScreen) -> any View {
switch screen {
case .externalReview:
break
case .webinarPresentation:
return WebinaireScreen()
case .profileCompletion:
return ProfileCompletionScreen()
case .editPerimeter:
return ResizePerimeterScreen()
case .onlyProSearches:
return OnlyProRequestsScreen()
case .oneMonthBoosterOffered:
return BoosterSubscriptionSelectionScreen()
case .allovoisinsPromotion:
return AlloVoisinReputationScreen()
case .extendMyTrialPeriod:
return MoreTimeScreen()
case .askPartProStatusChangeWithCancellation:
return StatusChangeScreen()
case .customerSupportForPro:
return PersonalizedSupportScreen()
case .customerSupportForPart:
return PersonalizedSupportScreen()
case .askPartProStatusChange:
return StatusChangeScreen()
case .ordersPresentation:
break
}
return EmptyView()
}
@ViewBuilder
func getPromotionalScreenNew(for screen: ResiliationPromotionalScreen) -> some View {
switch screen {
case .webinarPresentation:
WebinaireScreen()
case .profileCompletion:
ProfileCompletionScreen()
case .editPerimeter:
ResizePerimeterScreen()
case .onlyProSearches:
OnlyProRequestsScreen()
case .oneMonthBoosterOffered:
BoosterSubscriptionSelectionScreen()
case .allovoisinsPromotion:
AlloVoisinReputationScreen()
case .extendMyTrialPeriod:
MoreTimeScreen()
case .askPartProStatusChangeWithCancellation, .askPartProStatusChange:
StatusChangeScreen()
case .customerSupportForPro, .customerSupportForPart:
PersonalizedSupportScreen()
default:
EmptyView()
}
}
}

View File

@@ -8,16 +8,10 @@
import SwiftUI
struct BoosterActiveHeaderView: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View {
VStack {
Image("booster_logo", bundle: Bundle.module)
Image("booster_logo")
.resizable()
.frame(width: 93, height: 80)
VStack {
@@ -30,7 +24,7 @@ struct BoosterActiveHeaderView: View {
.frame(maxWidth: .infinity)
.overlay(
ZStack(alignment: .topLeading) {
Image("booster_corner", bundle: Bundle.module)
Image("booster_corner")
.resizable()
.frame(width: 50, height: 50)
.cornerRadius(8, corners: [.topLeft])

View File

@@ -10,13 +10,6 @@ import SwiftUI
struct BoosterHistoryCellView: View {
var isEnded: Bool = false
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View {
ZStack {
HStack(alignment: .center) {
@@ -28,7 +21,7 @@ struct BoosterHistoryCellView: View {
}
.overlay(
ZStack(alignment: .topLeading) {
Image("booster_corner", bundle: Bundle.module)
Image("booster_corner")
.resizable()
.frame(width: 50, height: 50)
.cornerRadius(8, corners: [.topLeft, .bottomLeft])

View File

@@ -8,13 +8,7 @@
import SwiftUI
struct BoosterLockedToPremierView: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View {
HStack {
SQIcon(.lock_keyhole, size: .xs, type: .solid, color: .white)

View File

@@ -25,7 +25,7 @@ struct BoosterPromotionView: View {
.foregroundColor(.sqRoyal())
.overlay(
ZStack(alignment: .topLeading) {
Image("booster_corner", bundle: Bundle.module)
Image("booster_corner")
.resizable()
.frame(width: 50, height: 50)
.cornerRadius(8, corners: .topLeft)

View File

@@ -8,13 +8,6 @@
import SwiftUI
struct BoosterStatsView: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View {
VStack(spacing: 8) {

View File

@@ -18,10 +18,6 @@ struct BoosterSubscriptionCardView: View {
}
init(id: Int, selectedId: Binding<Int>, currentOption: Bool = false, isFree: Bool = false) {
do {
try FontRegistration().registerFonts()
} catch {}
self.id = id
self._selectedId = selectedId
self.currentOption = currentOption
@@ -61,7 +57,7 @@ struct BoosterSubscriptionCardView: View {
.padding(.top, 30)
.padding([.leading, .trailing])
Image("booster_corner_light", bundle: Bundle.module)
Image("booster_corner_light")
.resizable()
.frame(width: 50, height: 50)
.position(x: 25, y: 25)

View File

@@ -8,13 +8,7 @@
import SwiftUI
struct BoosterSubscriptionOptionsView: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View {
VStack {
HStack {

View File

@@ -0,0 +1,36 @@
//
// ComplimentPillView.swift
//
//
// Created by Victor on 31/07/2024.
//
import SwiftUI
struct ComplimentPillView: View {
var body: some View {
HStack(spacing: 4) {
VStack {
SQIcon(.gen_euro_sign, size: .s, color: .sqGreen(100))
}
.padding(3)
.frame(width: 32, height: 32, alignment: .center)
.background(Color.sqGreen(10))
.cornerRadius(32)
SQText("Excellent rapport qualité/prix", size: 12, font: .demiBold)
}
.padding(.leading, 1)
.padding(.trailing, 16)
.padding(.vertical, 1)
.cornerRadius(40)
.overlay(
RoundedRectangle(cornerRadius: 40)
.inset(by: 0.5)
.stroke(Color.sqNeutral(20), lineWidth: 1)
)
}
}
#Preview {
ComplimentPillView()
}

View File

@@ -0,0 +1,46 @@
//
// ComplimentView.swift
//
//
// Created by Victor on 31/07/2024.
//
import SwiftUI
struct ComplimentView: View {
var body: some View {
ZStack(alignment: .topTrailing) {
VStack(spacing: 4) {
VStack {
SQIcon(.gen_euro_sign, size: .xl, color: .sqGreen(100))
}
.frame(width: 56, height: 56, alignment: .center)
.background(Color.sqGreen(10))
.cornerRadius(100)
SQText("Excellent rapport qualité/prix", size: 11, font: .demiBold)
.multilineTextAlignment(.center)
.foregroundColor(Color.sqNeutral(90))
}
.padding(.top, 4)
.frame(width: 87)
HStack {
SQText("1", size: 11, font: .bold)
.foregroundColor(Color.sqNeutral())
.padding(.horizontal, 6)
.frame(height: 16)
.background(.white)
.cornerRadius(30)
.overlay(
RoundedRectangle(cornerRadius: 30)
.stroke(Color.sqNeutral(60), lineWidth: 1)
)
}
.padding(.trailing, 10)
}
}
}
#Preview {
ComplimentView()
}

View File

@@ -0,0 +1,48 @@
//
// ProfileComplimentListView.swift
//
//
// Created by Victor on 31/07/2024.
//
import SwiftUI
private struct Compliment: Identifiable {
let id: Int
}
struct ProfileComplimentListView: View {
private let compliments: [Compliment] = [
Compliment(id: 1),
Compliment(id: 2),
Compliment(id: 3),
Compliment(id: 4),
Compliment(id: 5)
]
var body: some View {
VStack(alignment: .leading) {
SQText("Compliments reçus", size: 18, font: .bold)
.padding(.horizontal)
VStack(alignment: .center, spacing: 16) {
VStack(alignment: .leading) {
ForEach(Array(stride(from: 0, to: compliments.count, by: 3)), id: \.self) { index in
HStack(spacing: 24) {
ForEach(index ..< min(index + 3, compliments.count), id: \.self) { _ in
ComplimentView()
}
}
}
}
Rectangle()
.frame(height: 16)
.foregroundColor(Color.sqNeutral(10))
}
}
}
}
#Preview {
ProfileComplimentListView()
}

View File

@@ -8,11 +8,6 @@
import SwiftUI
struct NeighborBanner: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {}
}
var body: some View {
HStack(spacing: 8) {

View File

@@ -201,7 +201,7 @@ struct SQIcon: View {
}
var body: some View {
Image(type == .solid ? "\(name.rawValue)_solid" : name.rawValue, bundle: .module)
Image(type == .solid ? "\(name.rawValue)_solid" : name.rawValue)
.resizable()
.scaledToFit()
.frame(height: customSize ?? size.rawValue)

View File

@@ -0,0 +1,51 @@
//
// SQNavigationBar.swift
// AlloVoisinsSwiftUI
//
// Created by Victor on 14/10/2024.
//
import SwiftUI
struct SQNavigationBar: ViewModifier {
@Environment(\.presentationMode) var presentationMode
let title: String
func body(content: Content) -> some View {
content
.navigationBarBackButtonHidden(true)
.navigationBarTitle(title, displayMode: .inline)
.navigationBarItems(leading: backButton)
.onAppear {
configureNavigationBarAppearance()
}
}
private var backButton: some View {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "chevron.left")
.foregroundColor(.black)
.imageScale(.large)
}
}
private func configureNavigationBarAppearance() {
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = .white
appearance.shadowColor = .gray.withAlphaComponent(0.3)
appearance.titleTextAttributes = [.foregroundColor: UIColor.black]
UINavigationBar.appearance().standardAppearance = appearance
UINavigationBar.appearance().compactAppearance = appearance
UINavigationBar.appearance().scrollEdgeAppearance = appearance
}
}
extension View {
func sqNavigationBar(title: String) -> some View {
self.modifier(SQNavigationBar(title: title))
}
}

View File

@@ -0,0 +1,36 @@
//
// SQSocialBar.swift
//
//
// Created by Victor on 18/07/2024.
//
import SwiftUI
struct SQSocialBar: View {
var style: SQSocialBarStyle = .full
init(style: SQSocialBarStyle) {
self.style = style
}
var body: some View {
VStack {
HStack {
}
HStack {
}
}
}
}
enum SQSocialBarStyle {
case full
case compact
}
#Preview {
SQSocialBar(style: .full)
}

View File

@@ -0,0 +1,46 @@
//
// SQText.swift
//
//
// Created by Victor on 12/06/2024.
//
import SwiftUI
enum SQTextFont: String {
case medium = "TTChocolates-Medium"
case mediumItalic = "TTChocolates-MediumIt"
case demiBold = "TTChocolates-DemiBold"
case bold = "TTChocolates-Bold"
case boldItalic = "TTChocolates-Bold-Italic"
}
struct SQText: View {
var text: String
var size: CGFloat
var font: SQTextFont
var textColor: Color
init(_ text: String, size: CGFloat = 16, font: SQTextFont = .medium, textColor: Color = .sqNeutral(90)) {
self.text = text
self.size = size
self.font = font
self.textColor = textColor
}
var body: some View {
Text(text)
.font(.custom(font.rawValue, size: size))
.foregroundStyle(textColor)
}
}
#Preview {
VStack(spacing: 10) {
SQText("Hello world!", font: .medium)
SQText("Hello world!", font: .mediumItalic)
SQText("Hello world!", size: 18, font: .demiBold)
SQText("Hello world!", font: .bold)
SQText("Hello world!", size: 18, font: .boldItalic)
}
}

View File

@@ -27,9 +27,6 @@ struct SQToast: View {
}
init(_ title: String = "", content: String, style: SQToastStyle = .success, hasClose: Bool = false) {
do {
try FontRegistration().registerFonts()
} catch {}
self.title = title
self.content = content
self.style = style

View File

@@ -8,13 +8,7 @@
import SwiftUI
struct OnlyForPremierView: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View {
VStack(spacing: 8) {
VStack(alignment: .leading, spacing: 8) {

View File

@@ -8,13 +8,6 @@
import SwiftUI
struct RatingStarsView: View {
init() {
do {
try FontRegistration().registerFonts()
} catch {
}
}
var body: some View {
HStack(alignment: .bottom) {

View File

@@ -8,11 +8,46 @@
import SwiftUI
struct ResiliationCheckStepView: View {
var checkStep: ResiliationCheckStep
@Binding var isSelected: Bool
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
VStack(alignment: .leading) {
if isSelected {
Image("checked_neutral")
.frame(width: 20, height: 20)
} else {
Image("checkbox_unchecked")
.frame(width: 20, height: 20)
}
HStack(alignment: .center) {
Spacer()
Image(checkStep.image)
.resizable()
.scaledToFit()
.frame(height: 100)
Spacer()
}
HStack {
SQText(checkStep.text)
.multilineTextAlignment(.leading)
Spacer()
}
.frame(maxWidth: .infinity)
}
.padding()
.frame(maxWidth: .infinity)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.sqNeutral(30), lineWidth: 1)
)
.onTapGesture {
self.isSelected.toggle()
}
}
}
#Preview {
ResiliationCheckStepView()
ResiliationCheckStepView(checkStep: .marketing, isSelected: .constant(true))
.padding()
}

View File

@@ -1,84 +0,0 @@
//
// ResiliationCheckStepView.swift
// Sequoia
//
// Created by Victor on 10/10/2024.
//
import SwiftUI
private struct ResiliationCheckStepView: View {
var checkStep: ResiliationCheckStep
@Binding var isSelected: Bool
var body: some View {
VStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 4, style: .continuous)
.fill(isSelected ? Color.sqNeutral(100) : .clear)
.stroke(isSelected ? Color.sqNeutral(100) : Color.sqNeutral(30), lineWidth: 1)
.frame(width: 20, height: 20)
HStack(alignment: .center) {
Spacer()
Image(checkStep.image, bundle: Bundle.main)
.resizable()
.scaledToFit()
.frame(height: 100)
Spacer()
}
HStack {
SQText(checkStep.text)
.multilineTextAlignment(.leading)
Spacer()
}
.frame(maxWidth: .infinity)
}
.padding()
.frame(maxWidth: .infinity)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.sqNeutral(30), lineWidth: 1)
)
.onTapGesture {
self.isSelected.toggle()
}
}
}
struct ResiliationCheckStepsScreen: View {
@StateObject var viewModel: ResiliationViewModel
var steps: [ResiliationCheckStep] {
viewModel.resiliationCheckSteps
}
@State private var selectedSteps: Set<ResiliationCheckStep> = []
var allStepsSelected: Bool {
selectedSteps.count == steps.count
}
var body: some View {
ScrollView {
VStack(spacing: 16) {
ForEach(steps, id: \.self) { step in
ResiliationCheckStepView(
checkStep: step,
isSelected: Binding(
get: { selectedSteps.contains(step) },
set: { isSelected in
if isSelected {
selectedSteps.insert(step)
} else {
selectedSteps.remove(step)
}
}
)
)
}
}
.padding()
}
}
}
#Preview {
ResiliationCheckStepsScreen(viewModel: ResiliationViewModel(resiliationType: .apPart))
}

View File

@@ -9,7 +9,12 @@ import SwiftUI
struct TrialWarningCellView: View {
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
VStack(spacing: 8) {
SQIcon(.triangle_exclamation, size: .xxl, type: .solid, color: .SEMANTIC_RED)
SQText("Votre période dessai gratuite* se termine le 14 mars 2024. Toute résiliation avant cette date entraînera la coupure immédiate du service.", font: .demiBold)
.multilineTextAlignment(.center)
SQText("* Offre dessai valable une seule fois par utilisateur.", size: 13)
}
}
}

View File

@@ -0,0 +1,34 @@
//
// NewPerimeterCellView.swift
// Sequoia
//
// Created by Victor on 26/09/2024.
//
import SwiftUI
struct NewPerimeterCellView: View {
var body: some View {
VStack(spacing: 16) {
SQText("Vous souhaitez ajouter un nouveau périmètre ?", size: 24, font: .bold)
.multilineTextAlignment(.center)
SQText("Si vous souhaitez couvrir une zone géographique différente de votre périmètre actuel, souscrivez à un nouvel abonnement pour proposer vos services dans ce nouveau secteur.")
SQButton("Souscrire à un nouveau périmètre", color: .sqOrange(50), textColor: .sqOrange(50), isFull: false) {}
SQText("À partir de 9,99€ / mois, sans engagement", size: 13)
}
.padding(.horizontal, 32)
.padding(.vertical, 16)
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.inset(by: 0.5)
.stroke(Color.sqNeutral(20), lineWidth: 1)
)
}
}
#Preview {
NewPerimeterCellView()
.padding()
}