diff --git a/Permanent.xcodeproj/project.pbxproj b/Permanent.xcodeproj/project.pbxproj index 62e46364..4c4698fc 100644 --- a/Permanent.xcodeproj/project.pbxproj +++ b/Permanent.xcodeproj/project.pbxproj @@ -48,6 +48,9 @@ 5E07D45028915FB7008C0A6B /* RightSideMenuPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07D44F28915FB7008C0A6B /* RightSideMenuPage.swift */; }; 5E07D4522891600A008C0A6B /* PrivateFilesPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07D4512891600A008C0A6B /* PrivateFilesPage.swift */; }; 5E07D45528917D83008C0A6B /* UploadFilesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E07D45428917D83008C0A6B /* UploadFilesUITests.swift */; }; + 5E086BE62BEBEC91007B1AF3 /* OnboardingChartYourPathView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E086BE52BEBEC91007B1AF3 /* OnboardingChartYourPathView.swift */; }; + 5E086BE82BED2572007B1AF3 /* OnboardingPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E086BE72BED2572007B1AF3 /* OnboardingPath.swift */; }; + 5E086BEA2BEE4408007B1AF3 /* PathItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E086BE92BEE4408007B1AF3 /* PathItemView.swift */; }; 5E0ADB8D279052A500F39E7E /* PublicProfileLocationSetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0ADB8C279052A500F39E7E /* PublicProfileLocationSetViewController.swift */; }; 5E0B1BD42B961CE800B10BB5 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0B1BD32B961CE800B10BB5 /* OnboardingView.swift */; }; 5E0E3C2E2886DBCA001C7F10 /* ShareExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0E3C2D2886DBCA001C7F10 /* ShareExtensionTests.swift */; }; @@ -979,6 +982,9 @@ 5E07D44F28915FB7008C0A6B /* RightSideMenuPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RightSideMenuPage.swift; sourceTree = ""; }; 5E07D4512891600A008C0A6B /* PrivateFilesPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateFilesPage.swift; sourceTree = ""; }; 5E07D45428917D83008C0A6B /* UploadFilesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadFilesUITests.swift; sourceTree = ""; }; + 5E086BE52BEBEC91007B1AF3 /* OnboardingChartYourPathView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingChartYourPathView.swift; sourceTree = ""; }; + 5E086BE72BED2572007B1AF3 /* OnboardingPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPath.swift; sourceTree = ""; }; + 5E086BE92BEE4408007B1AF3 /* PathItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathItemView.swift; sourceTree = ""; }; 5E0ADB8C279052A500F39E7E /* PublicProfileLocationSetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicProfileLocationSetViewController.swift; sourceTree = ""; }; 5E0B1BD32B961CE800B10BB5 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; 5E0E3C2D2886DBCA001C7F10 /* ShareExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionTests.swift; sourceTree = ""; }; @@ -1859,6 +1865,8 @@ 5ED9A7B52BBF3294009BDD70 /* OnboardingSelectArchiveTypeView.swift */, 5EE3E9DF2BCD447100A5EFE5 /* OnboardingArchiveName.swift */, 5ED9A7B72BBF3BD6009BDD70 /* ArchiveTypeView.swift */, + 5E086BE52BEBEC91007B1AF3 /* OnboardingChartYourPathView.swift */, + 5E086BE92BEE4408007B1AF3 /* PathItemView.swift */, ); path = Views; sourceTree = ""; @@ -3698,6 +3706,7 @@ 5E3A6EE8275F610B00AD6DCC /* FieldNameUI.swift */, 5E781EA72787006B0007E397 /* ProfileItemOperation.swift */, 5E3A05FF29E44D7900A6C531 /* Workspace.swift */, + 5E086BE72BED2572007B1AF3 /* OnboardingPath.swift */, ); path = Enums; sourceTree = ""; @@ -4533,6 +4542,7 @@ 92E3FB512A17681A00E9E5A6 /* LegacyPlanningStatusViewController.swift in Sources */, 5E6673172A7A3A78001C49CC /* AppendFilenameViewModel.swift in Sources */, BC59BAEA25C2CC8E005A45D3 /* NotificationType.swift in Sources */, + 5E086BE82BED2572007B1AF3 /* OnboardingPath.swift in Sources */, F5853D2026E9FA5A0050E377 /* Permission.swift in Sources */, 5E181B822AF10100002DE69A /* GiftStorageViewModel.swift in Sources */, 5E9977B0285098FD003E0C46 /* AVCaptureDeviceExtension.swift in Sources */, @@ -4758,6 +4768,7 @@ BCE8DA7F256674D300842ABD /* BottomActionSheet.swift in Sources */, BC3DF862252DB8BA003D3829 /* LocalAuthErrors.swift in Sources */, BCD948D4258BA54600089F86 /* ItemVO.swift in Sources */, + 5E086BE62BEBEC91007B1AF3 /* OnboardingChartYourPathView.swift in Sources */, BC6D3B432513664C00390927 /* AuthViewModel.swift in Sources */, BC6D3B552514F26F00390927 /* APIError.swift in Sources */, 5E991EDF2A48E05C006229C0 /* MetadataEditView.swift in Sources */, @@ -4806,6 +4817,7 @@ F5853CFA26DEBB9D0050E377 /* PRMNTActionSheetViewController.swift in Sources */, BC59BAB425C2B7D6005A45D3 /* SharePreviewViewModelDelegate.swift in Sources */, 5E6FB7582A73E15800984B84 /* MetadataEditFileNamesView.swift in Sources */, + 5E086BEA2BEE4408007B1AF3 /* PathItemView.swift in Sources */, 5EDDAE5A2A9E312E00415D9A /* EditDateAndTimeView.swift in Sources */, 5EB4C6382BD2F00100561F0F /* CustomBorderTextField.swift in Sources */, 5EA94A122B3486B000AD67F5 /* BottomInvalidAlertMessageView.swift in Sources */, diff --git a/Permanent/Common/Base/SwiftUIViews/ButtonViews/RoundButtonRightImageView.swift b/Permanent/Common/Base/SwiftUIViews/ButtonViews/RoundButtonRightImageView.swift index 0dba9186..a0cb8c85 100644 --- a/Permanent/Common/Base/SwiftUIViews/ButtonViews/RoundButtonRightImageView.swift +++ b/Permanent/Common/Base/SwiftUIViews/ButtonViews/RoundButtonRightImageView.swift @@ -8,19 +8,20 @@ import SwiftUI struct RoundButtonRightImageView: View { enum ButtonType { - case fillColor, noColor + case fillColor, noColor, noBorder } var type: ButtonType = .fillColor var isDisabled: Bool = false var isLoading: Bool = false let text: String - let rightImage: Image = Image(.rightArrowShort) + var rightImage: Image = Image(.rightArrowShort) let action: () -> Void var body: some View { Button(action: action, label: { - if type == .fillColor { + switch type { + case .fillColor: ZStack { Color(.white) HStack() { @@ -53,8 +54,7 @@ struct RoundButtonRightImageView: View { .frame(height: 56) .cornerRadius(12) .frame(maxWidth: .infinity) - } - if type == .noColor { + case .noColor: ZStack { HStack() { if Constants.Design.isPhone { @@ -91,6 +91,39 @@ struct RoundButtonRightImageView: View { .stroke(.white.opacity(0.32), lineWidth: 1) ) + case .noBorder: + ZStack { + HStack() { + Spacer() + if Constants.Design.isPhone { + Text(text) + .textStyle(UsualSmallXMediumTextStyle()) + .foregroundColor(.white) + .lineLimit(1) + .minimumScaleFactor(0.8) + } else { + Text(text) + .textStyle(UsualRegularMediumTextStyle()) + .foregroundColor(.white) + .lineLimit(1) + .minimumScaleFactor(0.8) + } + if isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .frame(width: 24, height: 24) + } else { + rightImage + .frame(width: 24, height: 24, alignment: .center) + .accentColor(.white) + } + Spacer() + } + .padding(.horizontal, 24) + } + .frame(height: 56) + .cornerRadius(12) + .frame(maxWidth: .infinity) } }) .disabled(isDisabled || isLoading) diff --git a/Permanent/Common/Constants/Colors.swift b/Permanent/Common/Constants/Colors.swift index 323f0cb4..97649480 100644 --- a/Permanent/Common/Constants/Colors.swift +++ b/Permanent/Common/Constants/Colors.swift @@ -131,4 +131,13 @@ extension Gradient { startPoint: UnitPoint(x: 0, y: 0), endPoint: UnitPoint(x: 1, y: 1) ) + + static var lightDarkPurpleGradient = LinearGradient(stops: + [ + Gradient.Stop(color: Color(red: 0.5, green: 0, blue: 0.5), location: 0.00), + Gradient.Stop(color: Color(red: 0.72, green: 0.26, blue: 0.65), location: 1.00), + ], + startPoint: UnitPoint(x: 0, y: 0), + endPoint: UnitPoint(x: 1, y: 1) + ) } diff --git a/Permanent/Common/Models/Enums/OnboardingPath.swift b/Permanent/Common/Models/Enums/OnboardingPath.swift new file mode 100644 index 00000000..d80de4b4 --- /dev/null +++ b/Permanent/Common/Models/Enums/OnboardingPath.swift @@ -0,0 +1,34 @@ +// +// OnboardingPath.swift +// Permanent +// +// Created by Lucian Cerbu on 09.05.2024. + +import Foundation + +enum OnboardingPath: String, CaseIterable, Identifiable { + var id: String { return self.rawValue } + + case capture, digitize, collaborate, createPublicArchive, shareArchive, createPlan, organize, somethingElse = "" + + var description: String { + switch self { + case .capture: + return "Capture and preserve memories for storytelling" + case .digitize: + return "Digitize or transfer my materials securely" + case .collaborate: + return "Collaborate with others to build my archive" + case .createPublicArchive: + return "Create a public archive to share a legacy" + case .shareArchive: + return "Share my archive with others securely" + case .createPlan: + return "Create a plan for passing on my digital materials" + case .organize: + return "Organize my materials" + case .somethingElse: + return "Something else..." + } + } +} diff --git a/Permanent/Modules/Onboarding/ViewModel/OnboardingStorageValues.swift b/Permanent/Modules/Onboarding/ViewModel/OnboardingStorageValues.swift index ee81a990..6b08a438 100644 --- a/Permanent/Modules/Onboarding/ViewModel/OnboardingStorageValues.swift +++ b/Permanent/Modules/Onboarding/ViewModel/OnboardingStorageValues.swift @@ -10,6 +10,7 @@ import SwiftUI class OnboardingStorageValues: ObservableObject { @Published var archiveType: ArchiveType = .person @Published var textFieldText: String = "" + @Published var selectedPath: [OnboardingPath] = [] @Published var fullName: String = AuthenticationManager.shared.session?.account.fullName ?? "" let welcomeMessage: String = "We’re so glad you’re here!\n\nAt Permanent, it is our mission to provide a safe and secure place to store, preserve, and share the digital legacy of all people, whether that's for you or for your friends, family, interests or organizations.\n\nWe know that starting this journey can sometimes be overwhelming, but don’t worry. We’re here to help you every step of the way." @@ -20,4 +21,13 @@ class OnboardingStorageValues: ObservableObject { } return "a" } + + func togglePath(path: OnboardingPath) { + if let index = selectedPath.firstIndex(of: path) + { + selectedPath.remove(at: index) + } else { + selectedPath.append(path) + } + } } diff --git a/Permanent/Modules/Onboarding/Views/OnboardingChartYourPathView.swift b/Permanent/Modules/Onboarding/Views/OnboardingChartYourPathView.swift new file mode 100644 index 00000000..d343bfe4 --- /dev/null +++ b/Permanent/Modules/Onboarding/Views/OnboardingChartYourPathView.swift @@ -0,0 +1,118 @@ +// +// OnboardingChartYourPathView.swift +// Permanent +// +// Created by Lucian Cerbu on 08.05.2024. + +import SwiftUI + +struct OnboardingChartYourPathView: View { + @State var presentSelectArchivesType: Bool = false + @ObservedObject var onboardingValues: OnboardingStorageValues + + var backButton: (() -> Void) + var nextButton: (() -> Void) + var skipButton: (() -> Void) + @State var isKeyboardPresented = false + @State var textFieldText: String = "" + + var body: some View { + if Constants.Design.isPhone { + iPhoneBody + } else { + iPadBody + } + } + + var iPhoneBody: some View { + ZStack(alignment: .bottom) { + VStack { + ScrollViewReader { scrollReader in + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 24) { + Text("Chart your \npath to success") + .textStyle(UsualXLargeLightTextStyle()) + .foregroundColor(.white) + Text("Let’s set some goals. Everyone has unique goals for preserving their legacy. We want to learn more about how we can help you achieve yours.") + .textStyle(UsualSmallXRegularTextStyle()) + .foregroundColor(.blue25) + .lineSpacing(8.0) + VStack(alignment: .leading) { + ForEach(OnboardingPath.allCases, id: \.id) { path in + Button { + onboardingValues.togglePath(path: path) + } label: { + PathItemView(path: path, isSelected: onboardingValues.selectedPath.contains(path)) + } + } + } + } + } + } + Spacer(minLength: 160) + } + VStack { + RoundButtonRightImageView(type: .noBorder, text: "Skip this step", rightImage: Image(.onboardingForward), action: skipButton) + HStack(alignment: .center) { + SmallRoundButtonImageView(type: .noColor, imagePlace: .onLeft, text: "Back", image: Image(.leftArrowShort), action: backButton) + SmallRoundButtonImageView(isDisabled: onboardingValues.textFieldText.isEmpty, text: "Next", action: nextButton) + } + } + .padding(.bottom, 40) + .sheet(isPresented: $presentSelectArchivesType, content: { + OnboardingSelectArchiveTypeView(onboardingValues: onboardingValues) + }) + } + } + + var iPadBody: some View { + HStack(alignment: .top, spacing: 64) { + ScrollView(showsIndicators: false) { + VStack { + HStack() { + Text("Create your \n\(onboardingValues.archiveType.onboardingType) archive") + .textStyle(UsualXXLargeLightTextStyle()) + .foregroundColor(.white) + .multilineTextAlignment(.leading) + Spacer() + } + Spacer() + } + } + ZStack(alignment: .bottom) { + ScrollViewReader { scrollReader in + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 64) { + Text("Name your new archive. This is the legal or official name of the person, family, group, or organization the archive is about. You can edit the name later if needed.") + .textStyle(UsualRegularTextStyle()) + .foregroundColor(.blue25) + .lineSpacing(8.0) + CustomBorderTextField(textFieldText: $onboardingValues.textFieldText, placeholder: "Name...") + } + } + .onReceive(keyboardPublisher) { keyboard in + if keyboard.isFirstResponder { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { + withAnimation { + scrollReader.scrollTo(1, anchor: .center) + } + }) + } + } + .onAppear { + UIScrollView.appearance().bounces = false + } + .onDisappear { + UIScrollView.appearance().bounces = true + } + } + HStack(spacing: 32) { + SmallRoundButtonImageView(type: .noColor, imagePlace: .onLeft, text: "Back", image: Image(.backArrowOnboarding), action: backButton) + .frame(width: 120) + RoundButtonRightImageView(isDisabled: onboardingValues.textFieldText.isEmpty, text: "Create the archive", action: nextButton) + } + .padding(.bottom, 40) + } + } + } +} diff --git a/Permanent/Modules/Onboarding/Views/OnboardingView.swift b/Permanent/Modules/Onboarding/Views/OnboardingView.swift index 215409f4..77c1275f 100644 --- a/Permanent/Modules/Onboarding/Views/OnboardingView.swift +++ b/Permanent/Modules/Onboarding/Views/OnboardingView.swift @@ -12,7 +12,7 @@ struct OnboardingView: View { @State private var contentType: ContentType = .welcome enum ContentType { - case none, welcome, createArchive, setArchiveName + case none, welcome, createArchive, setArchiveName, chartYourPath } @State var bottomButtonsPadding: CGFloat = 40 @@ -76,6 +76,26 @@ struct OnboardingView: View { contentType = .createArchive } } nextButton: { + isBack = false + withAnimation { + contentType = .chartYourPath + } + } + .transition(AnyTransition.asymmetric( + insertion:.move(edge: isBack ? .leading : .trailing), + removal: .opacity) + ) + + case .chartYourPath: + OnboardingChartYourPathView(onboardingValues: onboardingValues) { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + isBack = true + withAnimation { + contentType = .setArchiveName + } + } nextButton: { + + } skipButton: { } .transition(AnyTransition.asymmetric( @@ -103,14 +123,21 @@ struct OnboardingView: View { DividerSmallBarView(type: .empty) DividerSmallBarView(type: .empty) DividerSmallBarView(type: .empty) + case .welcome: DividerSmallBarView(type: .empty) DividerSmallBarView(type: .empty) DividerSmallBarView(type: .empty) + case .createArchive, .setArchiveName: DividerSmallBarView(type: .gradient) DividerSmallBarView(type: .empty) DividerSmallBarView(type: .empty) + + case .chartYourPath: + DividerSmallBarView(type: .gradient) + DividerSmallBarView(type: .gradient) + DividerSmallBarView(type: .empty) } } .padding(.top, 24) diff --git a/Permanent/Modules/Onboarding/Views/PathItemView.swift b/Permanent/Modules/Onboarding/Views/PathItemView.swift new file mode 100644 index 00000000..9a6f1b23 --- /dev/null +++ b/Permanent/Modules/Onboarding/Views/PathItemView.swift @@ -0,0 +1,83 @@ +// +// PathItemView.swift +// Permanent +// +// Created by Lucian Cerbu on 10.05.2024. + +import SwiftUI + +struct PathItemView: View { + var path: OnboardingPath + var isSelected: Bool = false + + var body: some View { + if isSelected { + VStack(alignment: .leading) { + HStack(alignment: .top, spacing: 16) { + Image(.checkBoxCheckedFill) + .renderingMode(.template) + .frame(width: 24, height: 24) + .accentColor(.white) + if Constants.Design.isPhone { + Text("\(path.description)") + .textStyle(UsualSmallXRegularTextStyle()) + .foregroundColor(.white) + .lineSpacing(8.0) + .multilineTextAlignment(.leading) + } else { + Text("\(path.description)") + .textStyle(UsualMediumRegularTextStyle()) + .accentColor(.white) + .lineLimit(2) + .multilineTextAlignment(.leading) + } + Spacer() + } + .padding(.vertical, 16) + .padding(.horizontal, Constants.Design.isPhone ? 24 : 32) + } + .frame(height: Constants.Design.isPhone ? 96 : 120) + .background(Gradient.lightDarkPurpleGradient) + .cornerRadius(12) + .overlay( + RoundedRectangle(cornerRadius: 12) + .inset(by: 0.5) + .stroke(.white.opacity(0.16), lineWidth: 1) + ) + } else { + VStack(alignment: .leading) { + HStack(alignment: .top, spacing: 16) { + Image(.checkBoxEmpty) + .renderingMode(.template) + .frame(width: 24, height: 24) + .accentColor(.white) + if Constants.Design.isPhone { + Text("\(path.description)") + .textStyle(UsualSmallXRegularTextStyle()) + .foregroundColor(.white) + .lineLimit(2) + .lineSpacing(8.0) + .multilineTextAlignment(.leading) + } else { + Text("\(path.description)") + .textStyle(UsualMediumRegularTextStyle()) + .accentColor(.white) + .lineLimit(2) + .multilineTextAlignment(.leading) + } + Spacer() + } + .padding(.vertical, 16) + .padding(.horizontal, Constants.Design.isPhone ? 24 : 32) + } + .frame(height: Constants.Design.isPhone ? 96 : 120) + .frame(maxWidth: .infinity) + .cornerRadius(12) + .overlay( + RoundedRectangle(cornerRadius: 12) + .inset(by: 0.5) + .stroke(.white.opacity(0.16), lineWidth: 1) + ) + } + } +} diff --git a/Permanent/Resources/Assets/Assets.xcassets/Images/Onboading/onboardingForward.imageset/Contents.json b/Permanent/Resources/Assets/Assets.xcassets/Images/Onboading/onboardingForward.imageset/Contents.json new file mode 100644 index 00000000..ab3cebcf --- /dev/null +++ b/Permanent/Resources/Assets/Assets.xcassets/Images/Onboading/onboardingForward.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "onboardingForwardArrow2.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Permanent/Resources/Assets/Assets.xcassets/Images/Onboading/onboardingForward.imageset/onboardingForwardArrow2.svg b/Permanent/Resources/Assets/Assets.xcassets/Images/Onboading/onboardingForward.imageset/onboardingForwardArrow2.svg new file mode 100644 index 00000000..16b391d8 --- /dev/null +++ b/Permanent/Resources/Assets/Assets.xcassets/Images/Onboading/onboardingForward.imageset/onboardingForwardArrow2.svg @@ -0,0 +1,3 @@ + + + diff --git a/Permanent/Resources/Assets/Assets.xcassets/Images/checkboxEmpty 1.imageset/Contents.json b/Permanent/Resources/Assets/Assets.xcassets/Images/checkboxEmpty 1.imageset/Contents.json index f22fbd6e..689f8f5b 100644 --- a/Permanent/Resources/Assets/Assets.xcassets/Images/checkboxEmpty 1.imageset/Contents.json +++ b/Permanent/Resources/Assets/Assets.xcassets/Images/checkboxEmpty 1.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "filename" : "checkboxEmpty.svg", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : {