From 345e0acdec4eed587519c99bfb94c23c0e580620 Mon Sep 17 00:00:00 2001 From: Diogo Date: Tue, 26 Nov 2024 12:26:35 +0000 Subject: [PATCH] ios: onboarding redesign (#5252) * ios: onboarding redesign * shorter texts * updates * more updates * remove extra padding when focused * strings --------- Co-authored-by: Evgeny Poberezkin --- .../Onboarding/ChooseServerOperators.swift | 45 ++++--- .../Views/Onboarding/CreateProfile.swift | 64 ++++++---- .../Shared/Views/Onboarding/HowItWorks.swift | 6 +- .../Views/Onboarding/OnboardingView.swift | 8 +- .../Onboarding/SetNotificationsMode.swift | 112 ++++++++++++++---- .../Shared/Views/Onboarding/SimpleXInfo.swift | 72 ++++++----- .../NetworkAndServers/NetworkAndServers.swift | 3 +- .../UserSettings/NotificationsView.swift | 8 ++ apps/ios/SimpleXChat/APITypes.swift | 14 ++- 9 files changed, 210 insertions(+), 122 deletions(-) diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index 19d67bc62c..4efdb99f21 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -73,14 +73,20 @@ struct ChooseServerOperators: View { GeometryReader { g in ScrollView { VStack(alignment: .leading, spacing: 20) { - if !onboarding { - Text("Choose operators") - .font(.largeTitle) - .bold() + let title = Text("Server operators") + .font(.largeTitle) + .bold() + .frame(maxWidth: .infinity, alignment: .center) + + if onboarding { + title.padding(.top, 50) + } else { + title } infoText() - + .frame(maxWidth: .infinity, alignment: .center) + Spacer() ForEach(serverOperators) { srvOperator in @@ -117,11 +123,10 @@ struct ChooseServerOperators: View { .foregroundColor(.clear) } } - .font(.callout) - .padding(.top) + .font(.system(size: 17, weight: .semibold)) + .frame(minHeight: 40) } } - .padding(.bottom) if !onboarding && !reviewForOperators.isEmpty { VStack(spacing: 8) { @@ -162,21 +167,15 @@ struct ChooseServerOperators: View { } } .frame(maxHeight: .infinity) - .padding() + .padding(onboarding ? 25 : 16) } private func infoText() -> some View { - HStack(spacing: 12) { - Image(systemName: "info.circle") - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - .foregroundColor(theme.colors.primary) - .onTapGesture { - sheetItem = .showInfo - } - - Text("Select network operators to use.") + Button { + sheetItem = .showInfo + } label: { + Label("How it helps privacy", systemImage: "info.circle") + .font(.headline) } } @@ -305,8 +304,6 @@ struct ChooseServerOperators: View { private func notificationsModeDestinationView() -> some View { SetNotificationsMode() - .navigationTitle("Push notifications") - .navigationBarTitleDisplayMode(.large) .navigationBarBackButtonHidden(true) .modifier(ThemedBackground()) } @@ -334,7 +331,7 @@ struct ChooseServerOperators: View { .padding(.bottom) .padding(.bottom) } - .padding(.horizontal) + .padding(.horizontal, 25) .frame(maxHeight: .infinity) } @@ -411,7 +408,7 @@ struct ChooseServerOperators: View { struct ChooseServerOperatorsInfoView: View { var body: some View { VStack(alignment: .leading) { - Text("Network operators") + Text("Server operators") .font(.largeTitle) .bold() .padding(.vertical) diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index c6760319b1..7665e57cc1 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -119,49 +119,67 @@ struct CreateFirstProfile: View { var body: some View { VStack(alignment: .leading, spacing: 20) { - Text("Your profile, contacts and delivered messages are stored on your device.") - .font(.callout) - .foregroundColor(theme.colors.secondary) - Text("The profile is only shared with your contacts.") - .font(.callout) - .foregroundColor(theme.colors.secondary) + VStack(alignment: .center, spacing: 20) { + Text("Create your profile") + .font(.largeTitle) + .bold() + .multilineTextAlignment(.center) + + Text("Your profile, contacts and delivered messages are stored on your device.") + .font(.callout) + .foregroundColor(theme.colors.secondary) + .multilineTextAlignment(.center) + + Text("The profile is only shared with your contacts.") + .font(.callout) + .foregroundColor(theme.colors.secondary) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity) // Ensures it takes up the full width + .padding(.top, 25) + .padding(.horizontal, 10) HStack { let name = displayName.trimmingCharacters(in: .whitespaces) let validName = mkValidName(name) - ZStack { + ZStack(alignment: .trailing) { + TextField("Enter your name…", text: $displayName) + .focused($focusDisplayName) + .padding(.horizontal) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(Color(uiColor: .tertiarySystemFill)) + ) if name != validName { Button { showAlert(.invalidNameError(validName: validName)) } label: { - Image(systemName: "exclamationmark.circle").foregroundColor(.red) + Image(systemName: "exclamationmark.circle") + .foregroundColor(.red) + .padding(.horizontal, 10) } - } else { - Image(systemName: "exclamationmark.circle").foregroundColor(.clear) - Image(systemName: "pencil").foregroundColor(theme.colors.secondary) } } - TextField("Enter your name…", text: $displayName) - .focused($focusDisplayName) - .padding(.horizontal) - .padding(.vertical, 10) - .background( - RoundedRectangle(cornerRadius: 10, style: .continuous) - .fill(Color(uiColor: .tertiarySystemFill)) - ) } .padding(.top) Spacer() - createProfileButton() - .padding(.bottom) + VStack(spacing: 10) { + createProfileButton() + if !focusDisplayName { + onboardingButtonPlaceholder() + } + } } .onAppear() { focusDisplayName = true setLastVersionDefault() } - .padding() + .padding(.horizontal, 25) + .padding(.top, 10) + .padding(.bottom, 25) .frame(maxWidth: .infinity, alignment: .leading) } @@ -191,8 +209,6 @@ struct CreateFirstProfile: View { private func nextStepDestinationView() -> some View { ChooseServerOperators(onboarding: true) - .navigationTitle("Choose operators") - .navigationBarTitleDisplayMode(.large) .navigationBarBackButtonHidden(true) .modifier(ThemedBackground()) } diff --git a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift index 9a0ee4ddeb..66e63fd9c2 100644 --- a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift +++ b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift @@ -40,8 +40,10 @@ struct HowItWorks: View { Spacer() if onboarding { - createFirstProfileButton() - .padding(.bottom) + VStack(spacing: 10) { + createFirstProfileButton() + onboardingButtonPlaceholder() + } } } .lineLimit(10) diff --git a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift index d004e0306f..b2b1b8fa68 100644 --- a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift +++ b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift @@ -24,14 +24,10 @@ struct OnboardingView: View { CreateSimpleXAddress() case .step3_ChooseServerOperators: ChooseServerOperators(onboarding: true) - .navigationTitle("Choose operators") - .navigationBarTitleDisplayMode(.large) .navigationBarBackButtonHidden(true) .modifier(ThemedBackground()) case .step4_SetNotificationsMode: SetNotificationsMode() - .navigationTitle("Push notifications") - .navigationBarTitleDisplayMode(.large) .navigationBarBackButtonHidden(true) .modifier(ThemedBackground()) case .onboardingComplete: EmptyView() @@ -40,6 +36,10 @@ struct OnboardingView: View { } } +func onboardingButtonPlaceholder() -> some View { + Spacer().frame(height: 40) +} + enum OnboardingStage: String, Identifiable { case step1_SimpleXInfo case step2_CreateProfile // deprecated diff --git a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift index 91a755459a..cba290c286 100644 --- a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift +++ b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift @@ -13,41 +13,55 @@ struct SetNotificationsMode: View { @EnvironmentObject var m: ChatModel @State private var notificationMode = NotificationsMode.instant @State private var showAlert: NotificationAlert? + @State private var showInfo: Bool = false var body: some View { GeometryReader { g in ScrollView { - VStack(alignment: .leading, spacing: 20) { - Text("Send notifications:") + VStack(alignment: .center, spacing: 20) { + Text("Push Notifications") + .font(.largeTitle) + .bold() + .padding(.top, 50) + + infoText() + + Spacer() + ForEach(NotificationsMode.values) { mode in NtfModeSelector(mode: mode, selection: $notificationMode) } Spacer() - Button { - if let token = m.deviceToken { - setNotificationsMode(token, notificationMode) - } else { - AlertManager.shared.showAlertMsg(title: "No device token!") - } - onboardingStageDefault.set(.onboardingComplete) - m.onboardingStage = .onboardingComplete - } label: { - if case .off = notificationMode { - Text("Use chat") - } else { - Text("Enable notifications") + VStack(spacing: 10) { + Button { + if let token = m.deviceToken { + setNotificationsMode(token, notificationMode) + } else { + AlertManager.shared.showAlertMsg(title: "No device token!") + } + onboardingStageDefault.set(.onboardingComplete) + m.onboardingStage = .onboardingComplete + } label: { + if case .off = notificationMode { + Text("Use chat") + } else { + Text("Enable notifications") + } } + .buttonStyle(OnboardingButtonStyle()) + onboardingButtonPlaceholder() } - .buttonStyle(OnboardingButtonStyle()) - .padding(.bottom) } - .padding() + .padding(25) .frame(minHeight: g.size.height) } } .frame(maxHeight: .infinity) + .sheet(isPresented: $showInfo) { + NotificationsInfoView() + } } private func setNotificationsMode(_ token: DeviceToken, _ mode: NotificationsMode) { @@ -73,6 +87,15 @@ struct SetNotificationsMode: View { } } } + + private func infoText() -> some View { + Button { + showInfo = true + } label: { + Label("How it affects privacy", systemImage: "info.circle") + .font(.headline) + } + } } struct NtfModeSelector: View { @@ -83,15 +106,24 @@ struct NtfModeSelector: View { var body: some View { ZStack { - VStack(alignment: .leading, spacing: 4) { - Text(mode.label) - .font(.headline) + HStack(spacing: 16) { + Image(systemName: mode.icon) + .resizable() + .scaledToFill() + .frame(width: mode.icon == "bolt" ? 14 : 18, height: 18) .foregroundColor(selection == mode ? theme.colors.primary : theme.colors.secondary) - Text(ntfModeDescription(mode)) - .lineLimit(10) - .font(.subheadline) + VStack(alignment: .leading, spacing: 4) { + Text(mode.label) + .font(.headline) + .foregroundColor(selection == mode ? theme.colors.primary : theme.colors.secondary) + Text(ntfModeShortDescription(mode)) + .lineLimit(2) + .font(.callout) + } } - .padding(12) + .padding(.vertical, 12) + .padding(.trailing, 12) + .padding(.leading, 16) } .frame(maxWidth: .infinity, alignment: .leading) .background(tapped ? Color(uiColor: .secondarySystemFill) : theme.colors.background) @@ -107,6 +139,36 @@ struct NtfModeSelector: View { } } +struct NotificationsInfoView: View { + var body: some View { + VStack(alignment: .leading) { + Text("Notification modes") + .font(.largeTitle) + .bold() + .padding(.vertical) + ScrollView { + VStack(alignment: .leading) { + Group { + ForEach(NotificationsMode.values) { mode in + VStack(alignment: .leading, spacing: 4) { + Text(mode.label) + .font(.headline) + Text(ntfModeDescription(mode)) + .lineLimit(10) + .font(.callout) + } + } + } + .padding(.bottom) + } + } + } + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .modifier(ThemedBackground()) + } +} + struct NotificationsModeView_Previews: PreviewProvider { static var previews: some View { SetNotificationsMode() diff --git a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift index 2d90fb2fb2..b6d4c59279 100644 --- a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift +++ b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift @@ -19,21 +19,26 @@ struct SimpleXInfo: View { var body: some View { GeometryReader { g in ScrollView { - VStack(alignment: .leading, spacing: 20) { - Image(colorScheme == .light ? "logo" : "logo-light") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: g.size.width * 0.67) - .padding(.bottom, 8) - .frame(maxWidth: .infinity, minHeight: 48, alignment: .top) + VStack(alignment: .leading) { + VStack(alignment: .center, spacing: 10) { + Image(colorScheme == .light ? "logo" : "logo-light") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: g.size.width * 0.67) + .padding(.bottom, 8) + .frame(maxWidth: .infinity, minHeight: 48, alignment: .top) + + Button { + showHowItWorks = true + } label: { + Label("The future of messaging", systemImage: "info.circle") + .font(.headline) + } + } + + Spacer() VStack(alignment: .leading) { - Text("The next generation of private messaging") - .font(.title2) - .padding(.bottom, 30) - .padding(.horizontal, 40) - .frame(maxWidth: .infinity) - .multilineTextAlignment(.center) infoRow("privacy", "Privacy redefined", "The 1st platform without any user identifiers – private by design.", width: 48) infoRow("shield", "Immune to spam and abuse", @@ -45,25 +50,19 @@ struct SimpleXInfo: View { Spacer() if onboarding { - createFirstProfileButton() + VStack(spacing: 10) { + createFirstProfileButton() - Button { - m.migrationState = .pasteOrScanLink - } label: { - Label("Migrate from another device", systemImage: "tray.and.arrow.down") - .font(.subheadline) + Button { + m.migrationState = .pasteOrScanLink + } label: { + Label("Migrate from another device", systemImage: "tray.and.arrow.down") + .font(.system(size: 17, weight: .semibold)) + .frame(minHeight: 40) + } + .frame(maxWidth: .infinity) } - .frame(maxWidth: .infinity) } - - Button { - showHowItWorks = true - } label: { - Label("How it works", systemImage: "info.circle") - .font(.subheadline) - } - .frame(maxWidth: .infinity) - .padding(.bottom) } .frame(minHeight: g.size.height) } @@ -89,7 +88,9 @@ struct SimpleXInfo: View { } } .frame(maxHeight: .infinity) - .padding() + .padding(.horizontal, 25) + .padding(.top, 75) + .padding(.bottom, 25) } private func infoRow(_ image: String, _ title: LocalizedStringKey, _ text: LocalizedStringKey, width: CGFloat) -> some View { @@ -104,7 +105,7 @@ struct SimpleXInfo: View { .padding(.trailing, 10) VStack(alignment: .leading, spacing: 4) { Text(title).font(.headline) - Text(text).frame(minHeight: 40, alignment: .top) + Text(text).frame(minHeight: 40, alignment: .top).font(.callout) } } .padding(.bottom, 20) @@ -121,7 +122,7 @@ struct SimpleXInfo: View { .buttonStyle(OnboardingButtonStyle(isDisabled: false)) NavigationLink(isActive: $createProfileNavLinkActive) { - createProfileDestinationView() + CreateFirstProfile() } label: { EmptyView() } @@ -129,13 +130,6 @@ struct SimpleXInfo: View { .hidden() } } - - private func createProfileDestinationView() -> some View { - CreateFirstProfile() - .navigationTitle("Create your profile") - .navigationBarTitleDisplayMode(.large) - .modifier(ThemedBackground()) - } } struct SimpleXInfo_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift index 8b6421b502..16aa98bc5f 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift @@ -269,6 +269,7 @@ struct UsageConditionsView: View { } .padding(.bottom) .padding(.bottom) + case let .accepted(operators): Text("Conditions are accepted for the operator(s): **\(operators.map { $0.legalName_ }.joined(separator: ", "))**.") @@ -277,7 +278,7 @@ struct UsageConditionsView: View { .padding(.bottom) } } - .padding(.horizontal) + .padding(.horizontal, 25) .frame(maxHeight: .infinity) } diff --git a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift index b9c92c9919..ee43a24557 100644 --- a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift +++ b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift @@ -243,6 +243,14 @@ func ntfModeDescription(_ mode: NotificationsMode) -> LocalizedStringKey { } } +func ntfModeShortDescription(_ mode: NotificationsMode) -> LocalizedStringKey { + switch mode { + case .off: return "Check messages when allowed." + case .periodic: return "Check messages every 20 min." + case .instant: return "E2E encrypted notifications." + } +} + struct SelectionListView: View { @EnvironmentObject var theme: AppTheme var list: [Item] diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 51aa9108a1..f8cc2ac8b7 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -2158,9 +2158,17 @@ public enum NotificationsMode: String, Decodable, SelectableItem { public var label: LocalizedStringKey { switch self { - case .off: "Local" - case .periodic: "Periodically" - case .instant: "Instantly" + case .off: "No push server" + case .periodic: "Periodic" + case .instant: "Instant" + } + } + + public var icon: String { + switch self { + case .off: return "arrow.clockwise" + case .periodic: return "timer" + case .instant: return "bolt" } }