From eb99d7610826942ad19ef4db778a05818c655aec Mon Sep 17 00:00:00 2001 From: Victor Bodinaud Date: Wed, 26 Mar 2025 11:20:12 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8New=20adds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Views/MessageNotificationView.swift | 62 +++ .../Core/Views/Sequoia/AVAvatar.swift | 367 ++++++++++++++++++ .../Views/Sequoia/Components/SQPicker.swift | 10 +- .../Sequoia/Components/SQTextField.swift | 4 + .../{ => SqSearchBar}/SQSearchBar.swift | 0 .../SqSearchBar/SQSearchBarButton.swift | 34 ++ .../BoosterSubscriptionSelectionScreen.swift | 4 +- .../Views/Components/CurrentDebugUser.swift | 92 +++++ .../Views/ConfigPrestationSearchView.swift | 58 +++ .../DebugLand/Views/DebugLandView.swift | 100 +++++ .../Views/CategorySelectorView.swift | 2 + .../Views/Components/SubCategoryCell.swift | 33 ++ .../Views/PrestationSearchView.swift | 1 + .../Features/Subscriptions/Models/FAQ.swift | 53 +++ .../Subscriptions/Models/Pricing.swift | 83 ++++ .../Subscriptions/Models/PricingFeature.swift | 110 ++++++ .../Models/ReassuranceIndicator.swift | 35 ++ .../Views/Components/Pricing/FAQRow.swift | 37 ++ .../Views/Components/Pricing/FAQSection.swift | 24 ++ .../Views/Components/Pricing/FeatureRow.swift | 29 ++ .../Components/Pricing/FeatureSection.swift | 25 ++ .../Pricing/PricingSubscribeFooter.swift | 36 ++ .../Pricing/PricingSubscribeHeader.swift | 37 ++ .../Pricing/ReassuranceIndicatorSection.swift | 27 ++ .../Pricing/ReassuranceIndicatorsRow.swift | 24 ++ .../Components/Pricing/SwitchAndInfo.swift | 44 +++ .../Views/SubscriptionsPricingView.swift | 47 +++ .../Images/avatar-1.imageset/Avatar0.pdf | 185 +++++++++ .../Images/avatar-1.imageset/Contents.json | 12 + 29 files changed, 1573 insertions(+), 2 deletions(-) create mode 100644 AlloVoisinsSwiftUI/Core/Views/MessageNotificationView.swift create mode 100644 AlloVoisinsSwiftUI/Core/Views/Sequoia/AVAvatar.swift rename AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/{ => SqSearchBar}/SQSearchBar.swift (100%) create mode 100644 AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SqSearchBar/SQSearchBarButton.swift create mode 100644 AlloVoisinsSwiftUI/Features/DebugLand/Views/Components/CurrentDebugUser.swift create mode 100644 AlloVoisinsSwiftUI/Features/DebugLand/Views/ConfigPrestationSearchView.swift create mode 100644 AlloVoisinsSwiftUI/Features/DebugLand/Views/DebugLandView.swift create mode 100644 AlloVoisinsSwiftUI/Features/Prestations/Views/Components/SubCategoryCell.swift create mode 100644 AlloVoisinsSwiftUI/Features/Subscriptions/Models/FAQ.swift create mode 100644 AlloVoisinsSwiftUI/Features/Subscriptions/Models/Pricing.swift create mode 100644 AlloVoisinsSwiftUI/Features/Subscriptions/Models/PricingFeature.swift create mode 100644 AlloVoisinsSwiftUI/Features/Subscriptions/Models/ReassuranceIndicator.swift create mode 100644 AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/FAQRow.swift create mode 100644 AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/FAQSection.swift create mode 100644 AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/FeatureRow.swift create mode 100644 AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/FeatureSection.swift create mode 100644 AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/PricingSubscribeFooter.swift create mode 100644 AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/PricingSubscribeHeader.swift create mode 100644 AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/ReassuranceIndicatorSection.swift create mode 100644 AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/ReassuranceIndicatorsRow.swift create mode 100644 AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/SwitchAndInfo.swift create mode 100644 AlloVoisinsSwiftUI/Features/Subscriptions/Views/SubscriptionsPricingView.swift create mode 100644 AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/avatar-1.imageset/Avatar0.pdf create mode 100644 AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/avatar-1.imageset/Contents.json diff --git a/AlloVoisinsSwiftUI/Core/Views/MessageNotificationView.swift b/AlloVoisinsSwiftUI/Core/Views/MessageNotificationView.swift new file mode 100644 index 0000000..27d45aa --- /dev/null +++ b/AlloVoisinsSwiftUI/Core/Views/MessageNotificationView.swift @@ -0,0 +1,62 @@ +// +// MessageNotificationView.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 29/01/2025. +// + +import SwiftUI + +struct AVMessageNotification: Codable { + let displayName: String + let message: String + let avatarUrl: String + let actionUrl: String + let isNewUser: Bool + let countUnreadMessage: Int + + enum CodingKeys: String, CodingKey { + case displayName = "display_name" + case message + case avatarUrl = "avatar_url" + case actionUrl = "action_url" + case isNewUser = "is_new_user" + case countUnreadMessage = "count_unread_message" + } +} + +struct MessageNotificationView: View { + let notification: AVMessageNotification + + var body: some View { + HStack(alignment: .center, spacing: 8) { + AVAvatar(model: AVAvatarModel(avatarString: notification.avatarUrl, onlineStatus: .online(), sizing: .M)) + .frame(width: 48, height: 48) + VStack(alignment: .leading) { + SQText(notification.displayName, font: .demiBold) + SQText(notification.message, size: 14, textColor: .sqNeutral(70)) + .lineLimit(1) + } + Spacer() + VStack { + Circle() + .frame(width: 12, height: 12) + .foregroundStyle(Color.sqSemanticBlue) + .padding(.top, 8) + Spacer() + } + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .frame(height: 80) + .background { + RoundedRectangle(cornerRadius: 8) + .fill(Color.white) + .shadow(color: .black.opacity(0.15), radius: 8, x: 0, y: 4) + } + } +} + +#Preview { + MessageNotificationView(notification: AVMessageNotification(displayName: "Lucas E.", message: "Bonjour je voudrais une super voiture pour déplacer des ", avatarUrl: "https://public-allo10.allovoisins.com//assets/default_avatars/100/Avatar0.png", actionUrl: "", isNewUser: true, countUnreadMessage: 1)) +} diff --git a/AlloVoisinsSwiftUI/Core/Views/Sequoia/AVAvatar.swift b/AlloVoisinsSwiftUI/Core/Views/Sequoia/AVAvatar.swift new file mode 100644 index 0000000..2caad21 --- /dev/null +++ b/AlloVoisinsSwiftUI/Core/Views/Sequoia/AVAvatar.swift @@ -0,0 +1,367 @@ +// +// AVAvatar.swift +// Allovoisins +// +// Created by Florian Baudin on 15/03/2023. +// Copyright © 2023 AlloVoisins. All rights reserved. +// + +import SwiftUI + +enum AVOnlineState { + case online + case recently + case offline + case hourlyDispo +} + +class AVOnlineStatus: ObservableObject { + @Published var state: AVOnlineState! + @Published var lastConnection: Int? + + init(state: AVOnlineState, lastConnection: Int? = nil) { + self.state = state + self.lastConnection = lastConnection + } + + init(from lastOnlineMinutes: Int?, hourlyDispo: Bool = false) { + guard let lastOnlineMinutes else { + self.state = .offline + self.lastConnection = nil + return + } + + switch lastOnlineMinutes { + case 0 ... 5: + self.state = .online + self.lastConnection = lastOnlineMinutes + case 5 ..< 60: + self.state = .recently + self.lastConnection = lastOnlineMinutes + default: + self.state = .offline + self.lastConnection = nil + } + + guard hourlyDispo else { return } + self.state = .hourlyDispo + } + + static func online() -> AVOnlineStatus { + return AVOnlineStatus(state: .online, lastConnection: .zero) + } +} + +public class AVAvatarModel: ObservableObject { + enum Source { + case profile + case other + } + + @Published var onlineStatus: AVOnlineStatus + @Published var avatarString: String? + + var isNewUser: Bool + var isPro: Bool + var sizing: AVAvatarSizing + var bordered: Bool + var source: Source + + init(avatarString: String?, isNew: Bool = false, isPro: Bool = false, onlineStatus: AVOnlineStatus, sizing: AVAvatarSizing, bordered: Bool = false, source: Source = .other) { + self.avatarString = avatarString + self.isNewUser = isNew + self.isPro = isPro + self.onlineStatus = onlineStatus + self.sizing = sizing + self.bordered = bordered + self.source = source + } + + static func empty() -> AVAvatarModel { + return AVAvatarModel(avatarString: nil, + isNew: false, + isPro: false, + onlineStatus: AVOnlineStatus(state: .offline), + sizing: .M) + } +} + +struct AVAvatar: View { + @ObservedObject var model: AVAvatarModel + + var body: (some View)? { + ZStack { + Color.clear + self.avatarImage + .overlay(avatarBorder) + .overlay(newUserOverlay, alignment: .topLeading) + .overlay(onlineCircle, alignment: .bottomTrailing) + .frame(width: self.model.sizing.size, + height: self.model.sizing.size) + } + } + + private var avatarBorder: (some View)? { + Group { + if self.model.onlineStatus.state == .hourlyDispo && self.model.source != .profile { + Circle() + .stroke(Color.yellow, lineWidth: 3) + } else if self.model.bordered { + if self.model.source == .profile { + Circle() + .stroke(.white, lineWidth: 6) + } else { + Circle() + .stroke(.white, lineWidth: 4) + } + + } else { + EmptyView() + } + } + } + + private var avatarImage: (some View)? { + if #available(iOS 15.0, *) { + return AsyncImage( + url: URL(string: self.model.avatarString ?? "") + ) { image in + image + .resizable() + .clipShape(Circle()) + } placeholder: { + Image("AVAsset.Icon.DEFAULT_AVATAR") + .resizable() + .clipShape(Circle()) + } + } else { + return Image("") + .resizable() + .clipShape(Circle()) + } + } + + private var newUserOverlay: (some View)? { + Group { + if self.model.isNewUser && [.online, .offline].contains(self.model.onlineStatus.state) { + Image("") + .resizable() + .clipShape(Circle()) + .rotationEffect(Angle(degrees: 30)) + } else { + EmptyView() + } + } + } + + private var onlineCircle: (some View)? { + Group { + switch self.model.onlineStatus.state { + case .online: + let sizing = self.model.sizing.onlineSize + Circle() + .strokeBorder(Color.white, lineWidth: sizing.borderWidth) + .background(Circle().foregroundColor(Color.white)) + .frame(width: sizing.height, height: sizing.height) + .offset(x: sizing.padding.trailing, y: sizing.padding.bottom) + + case .recently: + let recentlyText = "\(self.model.onlineStatus.lastConnection ?? 1) min" + let sizing = self.model.sizing.recentlySize + SQText(recentlyText) + .padding(EdgeInsets(top: 0, leading: 3, bottom: 0, trailing: 2)) + .foregroundColor(Color.white) + .background(Color.white) + .cornerRadius(sizing.height / 2) + .overlay( + RoundedRectangle(cornerRadius: sizing.height / 2) + .stroke(.white, lineWidth: sizing.borderWidth) + ) + .frame(height: sizing.height) + + case .hourlyDispo: + let sizing = self.model.sizing.onlineSize + Image("") + .resizable() + .overlay(RoundedRectangle(cornerRadius: sizing.height / 2) + .stroke(.white, lineWidth: sizing.borderWidth)) + .background(Circle().foregroundColor(Color(.yellow))) + .frame(width: sizing.height, height: sizing.height) + .offset(x: sizing.padding.trailing, y: sizing.padding.bottom) + + default: + EmptyView() + } + } + } +} + +struct AVAvatar_Previews: PreviewProvider { + static var previews: some View { + VStack { + AVAvatar(model: AVAvatarModel( + avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg", + isNew: true, + onlineStatus: AVOnlineStatus(state: .offline), sizing: AVAvatarSizing.XS + )) + AVAvatar(model: AVAvatarModel( + avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg", + isNew: false, + onlineStatus: AVOnlineStatus(state: .offline, lastConnection: 59), sizing: AVAvatarSizing.XS + )) + + AVAvatar(model: AVAvatarModel( + avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg", + isNew: true, + onlineStatus: AVOnlineStatus(state: .online), sizing: AVAvatarSizing.S + )) + AVAvatar(model: AVAvatarModel( + avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg", + isNew: false, + onlineStatus: AVOnlineStatus(state: .recently, lastConnection: 59), sizing: AVAvatarSizing.S + )) + + AVAvatar(model: AVAvatarModel( + avatarString: nil, + isNew: true, + onlineStatus: AVOnlineStatus(state: .online), sizing: AVAvatarSizing.M + )) + AVAvatar(model: AVAvatarModel( + avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg", + isNew: false, + isPro: true, + onlineStatus: AVOnlineStatus(state: .recently, + lastConnection: 59), sizing: AVAvatarSizing.M, bordered: true + )) + + AVAvatar(model: AVAvatarModel( + avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg", + isNew: true, + onlineStatus: AVOnlineStatus(state: .hourlyDispo), sizing: AVAvatarSizing.L, bordered: true + )) + AVAvatar(model: AVAvatarModel( + avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg", + isNew: false, + onlineStatus: AVOnlineStatus(state: .recently, lastConnection: 59), sizing: AVAvatarSizing.L + )) + + AVAvatar(model: AVAvatarModel( + avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg", + isNew: true, + onlineStatus: AVOnlineStatus(state: .online), sizing: AVAvatarSizing.XL + )) + AVAvatar(model: AVAvatarModel( + avatarString: "https://preprod.allovoisins.com//uploads/u/avatars/9/b/9/9b93e04d6d_4681618_l.jpg", + isNew: false, + isPro: true, + onlineStatus: AVOnlineStatus(state: .recently, + lastConnection: 59), sizing: AVAvatarSizing.XL + )) + } + } +} + +enum AVAvatarSizing { + case XXS + case XS + case S + case M + case L + case XL + case XXL + + var size: CGFloat { + switch self { + case .XXS: + return 26 + case .XS: + return 32 + case .S: + return 40 + case .M: + return 48 + case .L: + return 64 + case .XL: + return 90 + case .XXL: + return 160 + } + } + + struct OnlineStateSize { + var height: CGFloat + var fontSize: CGFloat? = 0 + var borderWidth: CGFloat + var padding: EdgeSpace + + struct EdgeSpace { + var bottom: CGFloat + var trailing: CGFloat + } + } + + var onlineSize: OnlineStateSize { + switch self { + case .XXS, .XS: + return OnlineStateSize(height: .zero, + borderWidth: 0, + padding: OnlineStateSize.EdgeSpace(bottom: 0, + trailing: 0)) + case .S, .M: + return OnlineStateSize(height: 14, + borderWidth: 2, + padding: OnlineStateSize.EdgeSpace(bottom: 0, + trailing: 0)) + case .L: + return OnlineStateSize(height: 18, + borderWidth: 3, + padding: OnlineStateSize.EdgeSpace(bottom: 0, + trailing: 0)) + case .XL: + return OnlineStateSize(height: 18, + borderWidth: 3, + padding: OnlineStateSize.EdgeSpace(bottom: -3, + trailing: -3)) + case .XXL: + return OnlineStateSize(height: 30, + borderWidth: 4, + padding: OnlineStateSize.EdgeSpace(bottom: -8, + trailing: -8)) + } + } + + var recentlySize: OnlineStateSize { + switch self { + case .XXS, .XS: + return OnlineStateSize(height: .zero, + borderWidth: 0, + padding: OnlineStateSize.EdgeSpace(bottom: 0, + trailing: 0)) + case .S: + return OnlineStateSize(height: 13, + fontSize: 9, + borderWidth: 2, + padding: OnlineStateSize.EdgeSpace(bottom: 2, + trailing: 2)) + case .M: + return OnlineStateSize(height: 17, + fontSize: 11, + borderWidth: 2, + padding: OnlineStateSize.EdgeSpace(bottom: 2, + trailing: 2)) + case .L, .XL: + return OnlineStateSize(height: 17, + fontSize: 11, + borderWidth: 3, + padding: OnlineStateSize.EdgeSpace(bottom: -1, + trailing: 3)) + case .XXL: + return OnlineStateSize(height: 30, + fontSize: 18, + borderWidth: 4, + padding: OnlineStateSize.EdgeSpace(bottom: -3, + trailing: 0)) + } + } +} diff --git a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQPicker.swift b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQPicker.swift index 200e21a..7a4763e 100644 --- a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQPicker.swift +++ b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQPicker.swift @@ -40,8 +40,13 @@ struct SQPicker: View { } } .frame(maxWidth: .infinity) + .frame(height: 51) .fixedSize(horizontal: false, vertical: true) .tint(.sqNeutral(100)) + .background { + RoundedRectangle(cornerRadius: 8) + .fill(Color.white) + } } func triggerPickerMenu() { @@ -114,5 +119,8 @@ struct SQPickerPreview: View { } #Preview { - SQPickerPreview() + ZStack { + Color.black + SQPickerPreview() + } } diff --git a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQTextField.swift b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQTextField.swift index 5ff88fb..fbf5eae 100644 --- a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQTextField.swift +++ b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQTextField.swift @@ -135,6 +135,10 @@ struct SQTextField: View { } } .padding(16) + .background { + RoundedRectangle(cornerRadius: 8) + .fill(Color.white) + } .foregroundStyle(Color.sqNeutral()) .background(isDisabled ? Color.sqNeutral(10) : .clear) .overlay( diff --git a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQSearchBar.swift b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SqSearchBar/SQSearchBar.swift similarity index 100% rename from AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SQSearchBar.swift rename to AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SqSearchBar/SQSearchBar.swift diff --git a/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SqSearchBar/SQSearchBarButton.swift b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SqSearchBar/SQSearchBarButton.swift new file mode 100644 index 0000000..06f8cb7 --- /dev/null +++ b/AlloVoisinsSwiftUI/Core/Views/Sequoia/Components/SqSearchBar/SQSearchBarButton.swift @@ -0,0 +1,34 @@ +// +// SQSearchBarButton.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 08/01/2025. +// + +import SwiftUI + +struct SQSearchBarButton: View { + var placeholder: String + var action: () -> Void = {} + + var body: some View { + HStack(spacing: 16) { + SQIcon(.magnifying_glass, type: .solid) + + SQText(placeholder, textColor: .sqNeutral(50)) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .background(Color.sqNeutral(10)) + .cornerRadius(8) + .onTapGesture { + action() + } + } +} + +#Preview { + SQSearchBarButton(placeholder: "Rechercher") { + print("search") + } +} diff --git a/AlloVoisinsSwiftUI/Features/Booster/Views/BoosterSubscriptionSelectionScreen.swift b/AlloVoisinsSwiftUI/Features/Booster/Views/BoosterSubscriptionSelectionScreen.swift index f9aee68..8a28ab8 100644 --- a/AlloVoisinsSwiftUI/Features/Booster/Views/BoosterSubscriptionSelectionScreen.swift +++ b/AlloVoisinsSwiftUI/Features/Booster/Views/BoosterSubscriptionSelectionScreen.swift @@ -52,8 +52,10 @@ struct BoosterSubscriptionSelectionScreen: View { VStack(spacing: 48) { ZStack { ElipseShape() + .fill(Color.sqPurple(60)) + Rectangle() .fill(backgroundGradient) - + VStack(alignment: .trailing) { BoosterLockedToPremierView() VStack(spacing: 16) { diff --git a/AlloVoisinsSwiftUI/Features/DebugLand/Views/Components/CurrentDebugUser.swift b/AlloVoisinsSwiftUI/Features/DebugLand/Views/Components/CurrentDebugUser.swift new file mode 100644 index 0000000..cf71085 --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/DebugLand/Views/Components/CurrentDebugUser.swift @@ -0,0 +1,92 @@ +// +// CurrentDebugUser.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 15/01/2025. +// + +import SwiftUI + +struct CurrentDebugUser: View { + var body: some View { + VStack(spacing: 8) { + SQImage("avatar-1", height: 64) + VStack { + SQText("Harbin Leduc") + SQText("Torphy LLC") + SQText("Auto-entrepreneur", size: 12, font: .demiBold) + .padding(.vertical, 4) + .padding(.horizontal, 8) + .background { + RoundedRectangle(cornerRadius: 8) + .fill(Color.sqPurple(20)) + } + } + + HStack { + SQText("UserID:", font: .demiBold) + Spacer() + SQText("103135") + Button {} label: { + SQIcon(.files) + .padding(8) + .background { + Circle() + .fill(Color.sqNeutral(20)) + } + } + } + HStack { + SQText("Email:", font: .demiBold) + Spacer() + SQText("test@test.com") + Button {} label: { + SQIcon(.files) + .padding(8) + .background { + Circle() + .fill(Color.sqNeutral(20)) + } + } + } + HStack { + SQText("Téléphone:", font: .demiBold) + Spacer() + SQText("0612345678") + Button {} label: { + SQIcon(.files) + .padding(8) + .background { + Circle() + .fill(Color.sqNeutral(20)) + } + } + } +// HStack { +// SQText("Auth Token:", font: .demiBold) +// Spacer() +// SQText("fpoFZ4G4ZGhrhreF4Z6ZZG53") +// } +// HStack { +// SQText("Airship ID:", font: .demiBold) +// Spacer() +// SQText("34HZRH657653H636") +// } +// HStack { +// SQText("Firebase Token:", font: .demiBold) +// Spacer() +// SQText("rzogjiéG245G24gégéregezgrz") +// } + } + .padding() + .background { + RoundedRectangle(cornerRadius: 10) + .fill(Color.sqNeutral(10)) + } + .padding(.horizontal) + } +} + +#Preview { + CurrentDebugUser() +} diff --git a/AlloVoisinsSwiftUI/Features/DebugLand/Views/ConfigPrestationSearchView.swift b/AlloVoisinsSwiftUI/Features/DebugLand/Views/ConfigPrestationSearchView.swift new file mode 100644 index 0000000..4d5bc76 --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/DebugLand/Views/ConfigPrestationSearchView.swift @@ -0,0 +1,58 @@ +// +// ConfigPrestationSearchView.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 20/01/2025. +// + +import SwiftUI + +struct ConfigPrestationSearchView: View { + @State var showSearchView: Bool = false + @State var showSuggested: Bool = false + @State var showAllCategories: Bool = false + + @State var selectedSearchType: PickerOption = .init(text: "Prestations") + @State var searchTypes: [PickerOption] = [ + PickerOption(text: "Prestations"), + PickerOption(text: "Prestation Catégories"), + PickerOption(text: "Prestations + Prestation Catégories") + ] + + var body: some View { + VStack { + Spacer() + VStack(spacing: 16) { + SQText("Configuration de la recherche des prestations", size: 18, font: .bold) + .multilineTextAlignment(.center) + Divider() + SQText("Types à rechercher :", font: .demiBold) + SQPicker(selection: $selectedSearchType, options: searchTypes) + Toggle(isOn: $showSuggested) { + SQText("Afficher une suggestion", font: .demiBold) + } + Toggle(isOn: $showAllCategories) { + SQText("Afficher \"Toutes les catégories\"", font: .demiBold) + } + } + .padding() + .background { + RoundedRectangle(cornerRadius: 10) + .fill(Color.sqNeutral(10)) + } + .padding(.horizontal) + Spacer() + SQFooter { + SQButton("Afficher la vue", color: .sqNeutral(100), textColor: .white) { + showSearchView.toggle() + } + } + .sheet(isPresented: $showSearchView) {} + } + .ignoresSafeArea(.container, edges: .bottom) + } +} + +#Preview { + ConfigPrestationSearchView() +} diff --git a/AlloVoisinsSwiftUI/Features/DebugLand/Views/DebugLandView.swift b/AlloVoisinsSwiftUI/Features/DebugLand/Views/DebugLandView.swift new file mode 100644 index 0000000..a24d24a --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/DebugLand/Views/DebugLandView.swift @@ -0,0 +1,100 @@ +// +// DebugLandView.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 15/01/2025. +// + +import SwiftUI + +enum DebugEnvironment { + case allo1 + case allo2 + case preprod + case hotfix + case prod +} + +struct DebugLandView: View { + @State var currentEnv: PickerOption = .init(text: "Preprod") + @State var userId: String = "" + + var pickerOptions: [PickerOption] { + [ + PickerOption(text: "Allo1"), + PickerOption(text: "Allo2"), + PickerOption(text: "Allo3"), + PickerOption(text: "Allo4"), + PickerOption(text: "Allo5"), + PickerOption(text: "Allo6"), + PickerOption(text: "Allo7"), + PickerOption(text: "Allo8"), + PickerOption(text: "Allo9"), + PickerOption(text: "Allo10"), + PickerOption(text: "Preprod"), + PickerOption(text: "Hotfix"), + PickerOption(text: "Prod"), + ] + } + + var body: some View { + ScrollView { + VStack(spacing: 16) { + CurrentDebugUser() + + VStack { + SQText("Environnement :", font: .demiBold) + SQPicker(selection: $currentEnv, options: pickerOptions) + } + .padding() + .background { + RoundedRectangle(cornerRadius: 10) + .fill(Color.sqNeutral(10)) + } + .padding(.horizontal) + + VStack(alignment: .leading, spacing: 8) { + SQText("User ID :", font: .demiBold) + HStack(spacing: 8) { + VStack { + SQTextField("Visiter le profil :", placeholder: "UserID", text: $userId) + SQButton("Visiter le profil", color: .sqNeutral(100), textColor: .white) { + + } + } + VStack { + SQTextField("Se connecter sur :", placeholder: "UserID", text: $userId) + .keyboardType(.numberPad) + SQButton("Se connecter", color: .sqNeutral(100), textColor: .white) { + + } + } + } + } + .padding() + .background { + RoundedRectangle(cornerRadius: 10) + .fill(Color.sqNeutral(10)) + } + .padding(.horizontal) + } + } + .toolbar { + ToolbarItem(placement: .principal) { + SQText("🦄 Debug Land 🦄", font: .bold) + } + + ToolbarItem(placement: .primaryAction) { + Button {} label: { + SQIcon(.user_group) + } + } + } + } +} + +#Preview { + NavigationStack { + DebugLandView() + } +} diff --git a/AlloVoisinsSwiftUI/Features/Prestations/Views/CategorySelectorView.swift b/AlloVoisinsSwiftUI/Features/Prestations/Views/CategorySelectorView.swift index 790deb6..6f72e41 100644 --- a/AlloVoisinsSwiftUI/Features/Prestations/Views/CategorySelectorView.swift +++ b/AlloVoisinsSwiftUI/Features/Prestations/Views/CategorySelectorView.swift @@ -19,6 +19,7 @@ struct CategorySelectorView: View { Rectangle() .fill(Color.sqNeutral(100)) .frame(height: 2) + .padding(.top, 1) } VStack { SQText("Objets", font: .demiBold) @@ -26,6 +27,7 @@ struct CategorySelectorView: View { Rectangle() .fill(Color.sqNeutral(20)) .frame(height: 1) + .padding(.top, 1) } } } diff --git a/AlloVoisinsSwiftUI/Features/Prestations/Views/Components/SubCategoryCell.swift b/AlloVoisinsSwiftUI/Features/Prestations/Views/Components/SubCategoryCell.swift new file mode 100644 index 0000000..68a61c4 --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Prestations/Views/Components/SubCategoryCell.swift @@ -0,0 +1,33 @@ +// +// SubCategoryCell.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 08/01/2025. +// + +import SwiftUI + +struct SubCategoryCell: View { + var text: String + @Binding var isChecked: Bool + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + HStack { + if isChecked { + SQImage("checked_neutral", height: 20) + } else { + SQImage("checkbox_unchecked", height: 20) + } + SQText("Bricolage - Travaux") + } + .padding() + Divider() + .padding(.horizontal) + } + } +} + +#Preview { + SubCategoryCell(text: "Bricolage - Petits travaux", isChecked: .constant(true)) +} diff --git a/AlloVoisinsSwiftUI/Features/Prestations/Views/PrestationSearchView.swift b/AlloVoisinsSwiftUI/Features/Prestations/Views/PrestationSearchView.swift index 522b071..eb05f37 100644 --- a/AlloVoisinsSwiftUI/Features/Prestations/Views/PrestationSearchView.swift +++ b/AlloVoisinsSwiftUI/Features/Prestations/Views/PrestationSearchView.swift @@ -55,6 +55,7 @@ struct PrestationSearchView: View { } } + .padding(.vertical) } } .padding() diff --git a/AlloVoisinsSwiftUI/Features/Subscriptions/Models/FAQ.swift b/AlloVoisinsSwiftUI/Features/Subscriptions/Models/FAQ.swift new file mode 100644 index 0000000..6418a59 --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Subscriptions/Models/FAQ.swift @@ -0,0 +1,53 @@ +// +// FAQ.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 22/01/2025. +// + +import Foundation + +// Modèle pour une question fréquente +struct FAQItem: Identifiable { + let id = UUID() + let question: String + let answer: String +} + +// Extension de Pricing pour gérer les FAQs selon l'abonnement +extension Pricing { + var faqItems: [FAQItem] { + switch self { + case .standard: + return [] + case .premier: + return [ + FAQItem( + question: "Comment résilier mon abonnement Premier ?", + answer: "L'abonnement Premier est sans engagement. Vous pouvez le résilier à tout moment, depuis le menu Abonnement > Gérer mes abonnements > Demander la résiliation." + ) + ] + commonFAQItems + case .premierPro: + return [ + FAQItem( + question: "Comment résilier mon abonnement Premier ?", + answer: "Pendant votre essai gratuit de 14 jours, la résiliation est possible à tout moment. Passé ce délai, la résiliation sera effective à la fin de votre engagement de 12 mois. Vous pouvez résilier votre abonnement depuis le menu Abonnement > Gérer mes abonnements > Demander la résiliation." + ) + ] + commonFAQItems + } + } + + // Questions communes à tous les abonnements + private var commonFAQItems: [FAQItem] { + [ + FAQItem( + question: "Comment me faire payer mes prestations ?", + answer: "Paiement en ligne ou paiement en direct lors de la prestation, vous choisissez le mode de paiement qui vous convient. En optant pour le paiement en ligne, vous bénéficiez d'un paiement réalisé en toute sécurité, sans commission." + ), + FAQItem( + question: "Comment contacter le Service Clients ?", + answer: "Pour nous contacter, il suffit de vous rendre sur le lien \"Contactez-nous\" présent au bas de tous nos articles de notre aide en ligne.\nNotre Service Clients est disponible pour répondre à toutes vos questions, du lundi au vendredi, de 9h à 18h." + ) + ] + } +} diff --git a/AlloVoisinsSwiftUI/Features/Subscriptions/Models/Pricing.swift b/AlloVoisinsSwiftUI/Features/Subscriptions/Models/Pricing.swift new file mode 100644 index 0000000..88cadd2 --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Subscriptions/Models/Pricing.swift @@ -0,0 +1,83 @@ +// +// Pricing.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 21/01/2025. +// + +import SwiftUI + +enum Pricing: String, CaseIterable, Identifiable { + case standard + case premier + case premierPro + + var id: String { self.rawValue } + + var headerTitle: String { + switch self { + case .standard, .premier: + return "Abonnements" + case .premierPro: + return "Abonnement Premier" + } + } + + var footerButtonColor: Color { + switch self { + case .standard: + return .sqNeutral(100) + case .premier, .premierPro: + return .sqOrange(50) + } + } + + var footerTextColor: Color { + switch self { + case .standard: + return .sqNeutral(100) + case .premier, .premierPro: + return .sqOrange(70) + } + } + + var footerBackgroundColor: Color { + switch self { + case .standard: + return .sqNeutral(15) + case .premier, .premierPro: + return .sqOrange(20) + } + } + + var footerPrimaryText: String { + switch self { + case .standard: + return "Gratuit" + case .premier: + return "À partir de 9,99 € / mois" + case .premierPro: + return "Essai gratuit* de 14 jours" + } + } + + var footerSecondaryText: String { + switch self { + case .standard: + return "" + case .premier: + return "Sans engagement" + case .premierPro: + return "À partir de 29,99 € / mois" + } + } + + var footerTertiaryText: String { + switch self { + case .standard, .premier: + return "" + case .premierPro: + return "* Offre d’essai valable une seule fois par utilisateur." + } + } +} diff --git a/AlloVoisinsSwiftUI/Features/Subscriptions/Models/PricingFeature.swift b/AlloVoisinsSwiftUI/Features/Subscriptions/Models/PricingFeature.swift new file mode 100644 index 0000000..7dca3c1 --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Subscriptions/Models/PricingFeature.swift @@ -0,0 +1,110 @@ +// +// PricingFeature.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 22/01/2025. +// + +// Représente une fonctionnalité individuelle +struct PricingFeature { + let description: String + let value: FeatureValue + + enum FeatureValue { + case boolean(Bool) + case text(String) + } +} + +// Représente une section de fonctionnalités +struct PricingFeatureSection { + let title: String + let subtitle: String? + let features: [PricingFeature] +} + +// Extension de Pricing pour gérer les sections de fonctionnalités +extension Pricing { + var featureSections: [PricingFeatureSection] { + switch self { + case .standard: + // MARK: - Standard + return [ + PricingFeatureSection( + title: "Proposer mes services", + subtitle: "Répondez aux demandes dans votre périmètre d'intervention", + features: [ + PricingFeature(description: "Nombre de réponses", value: .text("jusqu'à 4 / mois")), + PricingFeature(description: "Demandes ayant déjà reçu 3 réponses", value: .boolean(false)), + PricingFeature(description: "Demandes réservées aux abonnés Premier", value: .boolean(false)) + ] + ), + PricingFeatureSection( + title: "Gérer la visibilité de mon profil", + subtitle: nil, + features: [ + PricingFeature(description: "Possibilité d'afficher mon numéro de téléphone", value: .boolean(false)), + PricingFeature(description: "Affichage des photos de mes réalisations", value: .text("3")), + PricingFeature(description: "Collecte d'avis auprès de mes anciens contacts", value: .boolean(false)), + PricingFeature(description: "Référencement prioritaire sur Google", value: .boolean(false)) + ] + ) + ] + case .premier: + // MARK: - Premier + return [ + PricingFeatureSection( + title: "Proposer mes services", + subtitle: "Répondez aux demandes dans votre périmètre d'intervention", + features: [ + PricingFeature(description: "Nombre de réponses", value: .text("illimité")), + PricingFeature(description: "Demandes ayant déjà reçu 3 réponses", value: .boolean(true)), + PricingFeature(description: "Demandes réservées aux abonnés Premier", value: .boolean(true)) + ] + ), + PricingFeatureSection( + title: "Gérer la visibilité de mon profil", + subtitle: nil, + features: [ + PricingFeature(description: "Possibilité d'afficher mon numéro de téléphone", value: .boolean(true)), + PricingFeature(description: "Affichage des photos de mes réalisations", value: .text("50")), + PricingFeature(description: "Collecte d'avis auprès de mes anciens contacts", value: .boolean(true)), + PricingFeature(description: "Référencement prioritaire sur Google", value: .boolean(true)) + ] + ) + ] + case .premierPro: + // MARK: - Premier Pro + return [ + PricingFeatureSection( + title: "Développer ma clientèle", + subtitle: "Répondez aux demandes dans votre périmètre d'intervention", + features: [ + PricingFeature(description: "Nombre de réponses", value: .text("illimité")), + PricingFeature(description: "Demandes ayant déjà reçu 3 réponses", value: .boolean(true)), + PricingFeature(description: "Demandes réservées aux Pros", value: .boolean(true)) + ] + ), + PricingFeatureSection( + title: "Augmenter ma visibilité", + subtitle: nil, + features: [ + PricingFeature(description: "Référencement prioritaire sur Google", value: .boolean(true)), + PricingFeature(description: "Affichage de mon numéro de téléphone\nsur mon profil", value: .boolean(true)), + PricingFeature(description: "Collecte d'avis auprès de mes anciens\nclients (hors AlloVoisins)", value: .boolean(true)), + PricingFeature(description: "Cartes de visite et prospectus\npersonnalisés", value: .boolean(true)) + ] + ), + PricingFeatureSection( + title: "Gagner du temps", + subtitle: nil, + features: [ + PricingFeature(description: "Logiciel de facturation intégré", value: .boolean(true)), + PricingFeature(description: "Signature électronique des documents", value: .boolean(true)), + PricingFeature(description: "Relance client automatisée", value: .boolean(true)) + ] + ) + ] + } + } +} diff --git a/AlloVoisinsSwiftUI/Features/Subscriptions/Models/ReassuranceIndicator.swift b/AlloVoisinsSwiftUI/Features/Subscriptions/Models/ReassuranceIndicator.swift new file mode 100644 index 0000000..578150e --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Subscriptions/Models/ReassuranceIndicator.swift @@ -0,0 +1,35 @@ +// +// ReassuranceIndicator.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 22/01/2025. +// + +import Foundation + +// Modèle pour un indicateur de réassurance +struct ReassuranceIndicator: Identifiable { + let id = UUID() + let icon: SQIconName + let text: String +} + +// Extension de Pricing pour gérer les indicateurs selon l'abonnement +extension Pricing { + var reassuranceIndicators: [ReassuranceIndicator] { + switch self { + case .standard, .premier: + return [ + ReassuranceIndicator(icon: .lock_keyhole_open, text: "Sans engagement"), + ReassuranceIndicator(icon: .euro_sign, text: "Pas de commission sur vos prestations"), + ReassuranceIndicator(icon: .comments, text: "Assistance prioritaire") + ] + case .premierPro: + return [ + ReassuranceIndicator(icon: .users, text: "4 millions de membres"), + ReassuranceIndicator(icon: .euro_sign, text: "Pas de commission sur vos prestations"), + ReassuranceIndicator(icon: .comments, text: "Assistance prioritaire") + ] + } + } +} diff --git a/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/FAQRow.swift b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/FAQRow.swift new file mode 100644 index 0000000..b383ba8 --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/FAQRow.swift @@ -0,0 +1,37 @@ +// +// FAQRow.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 22/01/2025. +// + +import SwiftUI + +struct FAQRow: View { + let item: FAQItem + @State private var isExpanded = false + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + Button(action: { + withAnimation { + isExpanded.toggle() + } + }) { + HStack { + SQText(item.question, size: 13, font: .demiBold) + Spacer() + SQIcon(isExpanded ? .chevron_up : .chevron_down) + } + .padding(.vertical, 16) + } + + if isExpanded { + SQText(item.answer, size: 14) + .padding(.bottom, 16) + } + + Divider() + } + } +} diff --git a/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/FAQSection.swift b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/FAQSection.swift new file mode 100644 index 0000000..fb0583e --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/FAQSection.swift @@ -0,0 +1,24 @@ +// +// FAQSection.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 22/01/2025. +// + +import SwiftUI + +struct FAQSection: View { + let items: [FAQItem] + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + SQText("Questions fréquentes", size: 16, font: .demiBold) + .padding(.vertical, 8) + + ForEach(items) { item in + FAQRow(item: item) + } + } + .padding(.horizontal) + } +} diff --git a/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/FeatureRow.swift b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/FeatureRow.swift new file mode 100644 index 0000000..695ca5f --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/FeatureRow.swift @@ -0,0 +1,29 @@ +// +// FeatureRow.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 22/01/2025. +// + +import SwiftUI + +struct FeatureRow: View { + let feature: PricingFeature + + var body: some View { + HStack(alignment: .top) { + SQText(" • ", size: 14) + SQText("\(feature.description)", size: 14) + Spacer() + switch feature.value { + case .boolean(let isIncluded): + SQIcon(isIncluded ? .check : .xmark, + size: .l, + type: .solid, + color: isIncluded ? .sqSemanticGreen : .sqSemanticRed) + case .text(let text): + SQText(text, size: 14, font: .demiBold) + } + } + } +} diff --git a/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/FeatureSection.swift b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/FeatureSection.swift new file mode 100644 index 0000000..38b4e72 --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/FeatureSection.swift @@ -0,0 +1,25 @@ +// +// FeatureSection.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 22/01/2025. +// + +import SwiftUI + +struct FeatureSection: View { + let section: PricingFeatureSection + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + SQText(section.title, size: 18, font: .bold) + if let subtitle = section.subtitle { + SQText(subtitle, size: 13, font: .demiBold) + } + ForEach(section.features, id: \.description) { feature in + FeatureRow(feature: feature) + } + } + .padding() + } +} diff --git a/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/PricingSubscribeFooter.swift b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/PricingSubscribeFooter.swift new file mode 100644 index 0000000..b0e1e95 --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/PricingSubscribeFooter.swift @@ -0,0 +1,36 @@ +// +// PricingSubscribeFooter.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 21/01/2025. +// + +import SwiftUI + +struct PricingSubscribeFooter: View { + var pricing: Pricing + + var body: some View { + VStack { + SQText(pricing.footerPrimaryText, size: 18, font: .bold, textColor: pricing.footerTextColor) + if !pricing.footerSecondaryText.isEmpty { + SQText(pricing.footerSecondaryText, size: 13, textColor: pricing.footerTextColor) + } + SQButton("Continuer", color: pricing.footerButtonColor, textColor: .white) { + + } + if !pricing.footerTertiaryText.isEmpty { + SQText(pricing.footerTertiaryText, size: 12, textColor: .sqOrange(70)) + } + } + .padding() + .frame(maxWidth: .infinity, alignment: .center) + .background(pricing.footerBackgroundColor) + .cornerRadius(8) + .shadow(color: Color(red: 0.09, green: 0.14, blue: 0.2).opacity(0.1), radius: 8, x: 0, y: -4) + } +} + +#Preview { + PricingSubscribeFooter(pricing: .standard) +} diff --git a/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/PricingSubscribeHeader.swift b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/PricingSubscribeHeader.swift new file mode 100644 index 0000000..6c29723 --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/PricingSubscribeHeader.swift @@ -0,0 +1,37 @@ +// +// PricingSubscribeHeader.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 21/01/2025. +// + +import SwiftUI + +struct PricingSubscribeHeader: View { + var pricing: Pricing + + var body: some View { + HStack { + Button { + + } label: { + SQIcon(.xmark, size: .l) + } + Spacer() + SQText(pricing.headerTitle, size: 18, font: .bold) + Spacer() + if pricing == .premierPro { + Button { + + } label: { + SQIcon(.user_group, size: .l) + } + } + } + .padding() + } +} + +#Preview { + PricingSubscribeHeader(pricing: .premier) +} diff --git a/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/ReassuranceIndicatorSection.swift b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/ReassuranceIndicatorSection.swift new file mode 100644 index 0000000..7095cf1 --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/ReassuranceIndicatorSection.swift @@ -0,0 +1,27 @@ +// +// ReassuranceIndicator.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 22/01/2025. +// + +import SwiftUI + +struct ReassuranceIndicatorSection: View { + let indicator: ReassuranceIndicator + + var body: some View { + VStack(spacing: 8) { + ZStack { + Circle() + .fill(Color.sqNeutral(15)) + .frame(width: 32, height: 32) + + SQIcon(indicator.icon, size: .s, type: .solid) + } + + SQText(indicator.text, size: 14) + .multilineTextAlignment(.center) + } + } +} diff --git a/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/ReassuranceIndicatorsRow.swift b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/ReassuranceIndicatorsRow.swift new file mode 100644 index 0000000..2e62c3c --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/ReassuranceIndicatorsRow.swift @@ -0,0 +1,24 @@ +// +// ReassuranceIndicatorsRow.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 22/01/2025. +// + +import SwiftUI + +struct ReassuranceIndicatorsRow: View { + let indicators: [ReassuranceIndicator] + + var body: some View { + HStack(spacing: 8) { + ForEach(indicators) { indicator in + ReassuranceIndicatorSection(indicator: indicator) + if indicator.id != indicators.last?.id { + Spacer() + } + } + } + .padding(.horizontal) + } +} diff --git a/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/SwitchAndInfo.swift b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/SwitchAndInfo.swift new file mode 100644 index 0000000..0c4f742 --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/Components/Pricing/SwitchAndInfo.swift @@ -0,0 +1,44 @@ +// +// SwitchAndInfo.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 21/01/2025. +// + +import SwiftUI + +struct SwitchAndInfo: View { + @Binding var pricing: Pricing + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + if pricing == .premierPro { + HStack { + SQIcon(.chart_line_up, size: .l) + SQText("L’abonnement conçu pour augmenter votre chiffre d’affaires et développer votre entreprise.", font: .demiBold) + } + .padding() + .background { + RoundedRectangle(cornerRadius: 8) + .foregroundStyle( Color.sqNeutral(10)) + } + } else { + Picker("", selection: $pricing) { + SQText("Standard").tag(Pricing.standard) + SQText("Premier").tag(Pricing.premier) + } + .pickerStyle(.segmented) + if pricing == .standard { + SQText("Idéal pour proposer vos services ponctuellement, sans objectif de complément de revenus.", font: .demiBold) + } else { + SQText("Idéal pour vous générer un complément de revenus régulier.", font: .demiBold) + } + } + } + .padding() + } +} + +#Preview { + SwitchAndInfo(pricing: .constant(.premier)) +} diff --git a/AlloVoisinsSwiftUI/Features/Subscriptions/Views/SubscriptionsPricingView.swift b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/SubscriptionsPricingView.swift new file mode 100644 index 0000000..b1fcd30 --- /dev/null +++ b/AlloVoisinsSwiftUI/Features/Subscriptions/Views/SubscriptionsPricingView.swift @@ -0,0 +1,47 @@ +// +// SubscriptionsPricingView.swift +// AlloVoisinsSwiftUI +// +// Created by Victor on 21/01/2025. +// + +import SwiftUI + +struct SubscriptionsPricingView: View { + @State var pricing: Pricing + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + PricingSubscribeHeader(pricing: pricing) + Divider() + ScrollView { + SwitchAndInfo(pricing: $pricing) + Divider() + + VStack(spacing: 0) { + ForEach(pricing.featureSections, id: \.title) { section in + FeatureSection(section: section) + } + } + + if pricing != .standard { + Divider() + ReassuranceIndicatorsRow(indicators: pricing.reassuranceIndicators) + .padding() + Divider() + FAQSection(items: pricing.faqItems) + } + } + + PricingSubscribeFooter(pricing: pricing) + } + .ignoresSafeArea(.container, edges: .bottom) + } +} + +#Preview { + Color.white + .sheet(isPresented: .constant(true)) { + SubscriptionsPricingView(pricing: .premierPro) + } +} diff --git a/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/avatar-1.imageset/Avatar0.pdf b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/avatar-1.imageset/Avatar0.pdf new file mode 100644 index 0000000..8c88f49 --- /dev/null +++ b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/avatar-1.imageset/Avatar0.pdf @@ -0,0 +1,185 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 300.000000 300.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.799255 0.843708 0.898039 scn +0.000000 150.000000 m +0.000000 232.842712 67.157288 300.000000 150.000000 300.000000 c +232.842712 300.000000 300.000000 232.842712 300.000000 150.000000 c +300.000000 67.157288 232.842712 0.000000 150.000000 0.000000 c +67.157288 0.000000 0.000000 67.157288 0.000000 150.000000 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 -16.041626 -56.863281 cm +0.467200 0.544662 0.640000 scn +148.459488 268.987030 m +162.539291 275.160522 177.380173 271.215851 186.499435 258.885834 c +249.168854 174.124954 l +257.207672 163.255920 259.575836 147.484070 255.444305 132.350998 c +229.972214 43.073334 l +223.329834 18.725861 202.148956 3.297180 182.670288 8.614502 c +25.247545 51.567444 l +5.768888 56.884735 -4.638077 80.929825 2.004305 105.277298 c +27.564735 194.867538 l +31.648695 209.830734 41.525627 222.113205 53.794498 227.491669 c +148.459488 268.987030 l +h +178.071045 156.238922 m +179.628311 155.843689 181.186005 155.448364 182.744797 155.053558 c +185.099365 154.458969 187.620422 155.801025 188.198013 158.162369 c +188.408676 159.032166 188.513992 159.959717 188.490219 160.945007 c +188.395081 164.791107 186.132248 168.423157 182.670059 170.101593 c +176.452377 173.122070 169.405670 169.632721 167.768005 163.309753 c +167.764603 163.292770 167.761200 163.279175 167.754410 163.262192 c +167.166626 160.934814 168.753311 158.600662 171.084091 158.009476 c +173.413559 157.421005 175.741806 156.830093 178.071045 156.238922 c +h +127.326241 105.638535 m +112.848907 109.389526 102.557457 121.807877 99.061287 137.311295 c +98.208481 141.089462 101.412453 144.843842 104.697968 143.991028 c +165.733154 128.178452 l +169.018661 127.325638 169.997192 122.490814 167.418381 119.602814 c +156.834732 107.748474 141.806976 101.887558 127.326241 105.638535 c +h +114.798615 187.724869 m +108.625107 190.592484 101.697327 187.109894 100.073257 180.844635 c +100.039284 180.712128 100.005302 180.583008 99.978127 180.450500 c +99.478668 178.116333 101.000809 175.816116 103.314606 175.221527 c +104.788635 174.843964 106.262665 174.465424 107.736877 174.086838 c +110.136459 173.470612 112.536537 172.854248 114.937920 172.241791 c +117.248314 171.647202 119.691216 172.934906 120.374138 175.221527 c +120.737686 176.437897 120.880386 177.779953 120.730896 179.254532 c +120.360550 182.873032 118.101120 186.189133 114.798615 187.724869 c +h +f* +n +Q + +endstream +endobj + +2 0 obj + 2432 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 300.000000 300.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 150.000000 m +0.000000 232.842712 67.157288 300.000000 150.000000 300.000000 c +232.842712 300.000000 300.000000 232.842712 300.000000 150.000000 c +300.000000 67.157288 232.842712 0.000000 150.000000 0.000000 c +67.157288 0.000000 0.000000 67.157288 0.000000 150.000000 c +h +f +n +Q + +endstream +endobj + +4 0 obj + 405 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q + +endstream +endobj + +7 0 obj + 46 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 300.000000 300.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000002692 00000 n +0000002715 00000 n +0000003370 00000 n +0000003392 00000 n +0000003690 00000 n +0000003792 00000 n +0000003813 00000 n +0000003988 00000 n +0000004062 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +4122 +%%EOF \ No newline at end of file diff --git a/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/avatar-1.imageset/Contents.json b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/avatar-1.imageset/Contents.json new file mode 100644 index 0000000..9a30712 --- /dev/null +++ b/AlloVoisinsSwiftUI/Resources/Assets.xcassets/Images/avatar-1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Avatar0.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +}