ios: onboarding redesign (#5252)

* ios: onboarding redesign

* shorter texts

* updates

* more updates

* remove extra padding when focused

* strings

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Diogo
2024-11-26 12:26:35 +00:00
committed by GitHub
co-authored by Evgeny Poberezkin
parent 1f04984a34
commit 345e0acdec
9 changed files with 210 additions and 122 deletions
@@ -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)
@@ -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())
}
@@ -40,8 +40,10 @@ struct HowItWorks: View {
Spacer()
if onboarding {
createFirstProfileButton()
.padding(.bottom)
VStack(spacing: 10) {
createFirstProfileButton()
onboardingButtonPlaceholder()
}
}
}
.lineLimit(10)
@@ -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
@@ -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()
@@ -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 {
@@ -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)
}
@@ -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<Item: SelectableItem>: View {
@EnvironmentObject var theme: AppTheme
var list: [Item]
+11 -3
View File
@@ -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"
}
}