Merge branch 'feature/3707_login_flow_new_designs' into 'develop'

MESSENGER-3707 new login flow and welcome integration

See merge request bwmessenger/bundesmessenger/bundesmessenger-ios!55
This commit is contained in:
Frank Rotermund
2022-11-30 17:36:55 +00:00
15 changed files with 172 additions and 41 deletions
+4 -1
View File
@@ -495,7 +495,7 @@ class BWIBuildSettings: NSObject {
// MARK: - Message Bubbles bwi show in our menu and only for beta builds
var bwiShowMessageBubbleSettings = false
var bwiShowMessageBubbleSettings = true
// bwi disable encrypted option in message context menu
var roomContextualMenuShowEncryptionOption = false
@@ -507,4 +507,7 @@ class BWIBuildSettings: NSObject {
var showSideMenuCoachMessage = false
var roomMembersAllowUserVerification = false
// one flag for all layout changes to the element login flow
var bumLoginFlowLayout = true
}
@@ -26,7 +26,6 @@ extension BWIBuildSettings {
bwiPersonalState = true
bwiEnableErrorTracking = true
bwiMatomoTrackingDefaultState = false
bwiShowMessageBubbles = true
showSessionManager = true
locationSharingEnabled = false
bwiLocationShareButtonVisible = false
+3
View File
@@ -540,3 +540,6 @@
"welcome_experience_start" = "Loslegen";
// MARK - new login flow
"authentication_server_selection_login_title" = "Willkommen!";
"authentication_login_username" = "Nutzername";
+22
View File
@@ -389,3 +389,25 @@
"poll_edit_form_poll_type" = "Poll type";
"poll_edit_form_poll_type_closed" = "Hidden Poll";
"poll_edit_form_poll_type_open" = "Open poll";
// MARK: - Welcome Experience
"welcome_experience_title1" = "Willkommen beim BundesMessenger";
"welcome_experience_description1" = "Aus Deutschland, für Deutschland: Der souveräne Messenger für die besonderen Anforderungen des öffentlichen Sektors.";
"welcome_experience_title2" = "Sicher und von überall";
"welcome_experience_description2" = "Von überall auf die eigenen Chats zugreifen und nahtlos zwischen Smartphone, Tablet und Computer wechseln. Und: Alle Daten sind Ende-zu-Ende verschlüsselt auf eigenen Servern gespeichert.";
"welcome_experience_title3" = "Einfach vernetzen";
"welcome_experience_description3" = "Kolleg*innen aus der gesamten Organisation sofort finden, und zwar ohne erst nach Kontaktdaten suchen zu müssen.";
"welcome_experience_title4" = "Chatten wie gewohnt";
"welcome_experience_description4" = "Text, Bilder, Videos, Dokumente, Standorte oder Sprachnachrichten versenden und vieles mehr an einzelne Personen oder in Gruppen. Und auf Wunsch auch über die Organisationsgrenze hinweg.";
"welcome_experience_title5" = "Gleich loslegen?";
"welcome_experience_description5" = "Ob deine Organisation den BundesMessenger schon eingeführt hat, findest du auf der nächsten Seite heraus. Falls nicht: Überzeuge deinen Admin doch einfach!";
"welcome_experience_start" = "Loslegen";
// MARK: - new login flow
"authentication_server_selection_login_title" = "Welcome!";
"authentication_login_username" = "Username";
+56
View File
@@ -27,6 +27,14 @@ public class BWIL10n: NSObject {
public static var authUserIdPlaceholder: String {
return BWIL10n.tr("Bwi", "auth_user_id_placeholder")
}
/// Nutzername
public static var authenticationLoginUsername: String {
return BWIL10n.tr("Bwi", "authentication_login_username")
}
/// Willkommen!
public static var authenticationServerSelectionLoginTitle: String {
return BWIL10n.tr("Bwi", "authentication_server_selection_login_title")
}
/// PIN eingeben
public static var biometricsModeCantUnlockButtonTitle: String {
return BWIL10n.tr("Bwi", "biometrics_mode_cant_unlock_button_title")
@@ -771,6 +779,10 @@ public class BWIL10n: NSObject {
public static var locationSharingCopyrightLabel: String {
return BWIL10n.tr("Bwi", "location_sharing_copyright_label")
}
/// Mapbox Maps SDK für iOS
public static var locationSharingMapCreditsTitle: String {
return BWIL10n.tr("Bwi", "location_sharing_map_credits_title")
}
/// Diagnosedaten senden
public static var matomoSettingsSendDiagnosticData: String {
return BWIL10n.tr("Bwi", "MATOMO_SETTINGS_SEND_DIAGNOSTIC_DATA")
@@ -1503,6 +1515,50 @@ public class BWIL10n: NSObject {
public static var warning: String {
return BWIL10n.tr("Bwi", "warning")
}
/// Aus Deutschland, für Deutschland: Der souveräne Messenger für die besonderen Anforderungen des öffentlichen Sektors.
public static var welcomeExperienceDescription1: String {
return BWIL10n.tr("Bwi", "welcome_experience_description1")
}
/// Von überall auf die eigenen Chats zugreifen und nahtlos zwischen Smartphone, Tablet und Computer wechseln. Und: Alle Daten sind Ende-zu-Ende verschlüsselt auf eigenen Servern gespeichert.
public static var welcomeExperienceDescription2: String {
return BWIL10n.tr("Bwi", "welcome_experience_description2")
}
/// Kolleg*innen aus der gesamten Organisation sofort finden, und zwar ohne erst nach Kontaktdaten suchen zu müssen.
public static var welcomeExperienceDescription3: String {
return BWIL10n.tr("Bwi", "welcome_experience_description3")
}
/// Text, Bilder, Videos, Dokumente, Standorte oder Sprachnachrichten versenden und vieles mehr an einzelne Personen oder in Gruppen. Und auf Wunsch auch über die Organisationsgrenze hinweg.
public static var welcomeExperienceDescription4: String {
return BWIL10n.tr("Bwi", "welcome_experience_description4")
}
/// Ob deine Organisation den BundesMessenger schon eingeführt hat, findest du auf der nächsten Seite heraus. Falls nicht: Überzeuge deinen Admin doch einfach!
public static var welcomeExperienceDescription5: String {
return BWIL10n.tr("Bwi", "welcome_experience_description5")
}
/// Loslegen
public static var welcomeExperienceStart: String {
return BWIL10n.tr("Bwi", "welcome_experience_start")
}
/// Willkommen beim BundesMessenger
public static var welcomeExperienceTitle1: String {
return BWIL10n.tr("Bwi", "welcome_experience_title1")
}
/// Sicher und von überall
public static var welcomeExperienceTitle2: String {
return BWIL10n.tr("Bwi", "welcome_experience_title2")
}
/// Einfach vernetzen
public static var welcomeExperienceTitle3: String {
return BWIL10n.tr("Bwi", "welcome_experience_title3")
}
/// Chatten wie gewohnt
public static var welcomeExperienceTitle4: String {
return BWIL10n.tr("Bwi", "welcome_experience_title4")
}
/// Gleich loslegen?
public static var welcomeExperienceTitle5: String {
return BWIL10n.tr("Bwi", "welcome_experience_title5")
}
}
// swiftlint:enable function_parameter_count identifier_name line_length type_body_length
+5
View File
@@ -50,6 +50,11 @@ internal class Asset: NSObject {
internal static let birthdayCake = ImageAsset(name: "birthday_cake")
internal static let fileAttachmentIcon = ImageAsset(name: "file_attachment_icon")
internal static let fileScanInfected = ImageAsset(name: "file_scan_infected")
internal static let welcomeExperience1 = ImageAsset(name: "welcome_experience_1")
internal static let welcomeExperience2 = ImageAsset(name: "welcome_experience_2")
internal static let welcomeExperience3 = ImageAsset(name: "welcome_experience_3")
internal static let welcomeExperience4 = ImageAsset(name: "welcome_experience_4")
internal static let welcomeExperience5 = ImageAsset(name: "welcome_experience_5")
internal static let callAudioMuteOffIcon = ImageAsset(name: "call_audio_mute_off_icon")
internal static let callAudioMuteOnIcon = ImageAsset(name: "call_audio_mute_on_icon")
internal static let callAudioRouteBuiltin = ImageAsset(name: "call_audio_route_builtin")
@@ -160,16 +160,29 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
/// Displays the next view in the flow after the splash screen.
private func bwiSplashScreenCoordinator(_ coordinator: OnboardingBwiSplashScreenCoordinator, didCompleteWith result: OnboardingBwiSplashScreenViewModelResult) {
startLoading()
if BWIBuildSettings.shared.onboardingEnableNewAuthenticationFlow {
beginAuthentication(with: .login) { [weak self] in
self?.stopLoading()
}
if BWIBuildSettings.shared.onboardingEnableNewAuthenticationFlow {
self.showWelcomeExperience()
} else {
showLegacyAuthenticationScreen(forceAsRootModule: true)
}
}
private func showWelcomeExperience() {
let welcomeVC = WelcomeExperienceViewController.makeViewController(completion: { [weak self] in
if BWIBuildSettings.shared.onboardingEnableNewAuthenticationFlow {
self?.startLoading()
self?.beginAuthentication(with: .login) { [weak self] in
self?.stopLoading()
}
} else {
self?.showLegacyAuthenticationScreen(forceAsRootModule: true)
}
})
self.navigationRouter.setRootModule(welcomeVC)
}
/// Show an empty screen when configuring soft logout flow
private func showEmptyScreen() {
MXLog.debug("[OnboardingCoordinator] showEmptyScreen")
@@ -37,23 +37,29 @@ struct AuthenticationServerInfoSection: View {
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(theme.fonts.subheadline)
.foregroundColor(theme.colors.secondaryContent)
if !BWIBuildSettings.shared.bumLoginFlowLayout {
Text(title)
.font(theme.fonts.subheadline)
.foregroundColor(theme.colors.secondaryContent)
}
HStack {
Text(address)
.font(theme.fonts.body)
.foregroundColor(theme.colors.primaryContent)
Spacer()
Button(action: editAction) {
Text(VectorL10n.edit)
.font(theme.fonts.body)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.overlay(RoundedRectangle(cornerRadius: 8).stroke(theme.colors.accent))
if !BWIBuildSettings.shared.bumLoginFlowLayout {
Button(action: editAction) {
Text(VectorL10n.edit)
.font(theme.fonts.body)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.overlay(RoundedRectangle(cornerRadius: 8).stroke(theme.colors.accent))
}
}
}
}
@@ -220,6 +220,12 @@ class AuthenticationService: NSObject {
/// - Parameter homeserverAddress: The homeserver string entered by the user.
/// - Returns: A tuple containing the REST client for the server along with the homeserver state containing the login flows.
private func loginFlow(for homeserverAddress: String) async throws -> (AuthenticationRestClient, AuthenticationState.Homeserver) {
// bwi: we start with an empty url, element tries to load a wellknown from localhost for 15 seconds in that case
guard homeserverAddress.count > 0 else {
throw AuthenticationError.invalidHomeserver
}
let homeserverAddress = HomeserverAddress.sanitized(homeserverAddress)
guard var homeserverURL = URL(string: homeserverAddress) else {
@@ -98,7 +98,7 @@ struct AuthenticationLoginScreen: View {
/// The form with text fields for username and password, along with a submit button.
var loginForm: some View {
VStack(spacing: 14) {
RoundedBorderTextField(placeHolder: VectorL10n.authenticationLoginUsername,
RoundedBorderTextField(placeHolder: BWIL10n.authenticationLoginUsername,
text: $viewModel.username,
isFirstResponder: false,
configuration: UIKitTextInputConfiguration(returnKeyType: .next,
@@ -118,13 +118,15 @@ struct AuthenticationLoginScreen: View {
onCommit: submit)
.accessibilityIdentifier("passwordTextField")
Button { viewModel.send(viewAction: .forgotPassword) } label: {
Text(VectorL10n.authenticationLoginForgotPassword)
.font(theme.fonts.body)
if !BWIBuildSettings.shared.bumLoginFlowLayout {
Button { viewModel.send(viewAction: .forgotPassword) } label: {
Text(VectorL10n.authenticationLoginForgotPassword)
.font(theme.fonts.body)
}
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(.bottom, 8)
}
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(.bottom, 8)
Button(action: submit) {
Text(VectorL10n.next)
}
@@ -38,7 +38,7 @@ struct AuthenticationServerSelectionViewState: BindableState {
var hasModalPresentation: Bool
var headerTitle: String {
flow == .login ? VectorL10n.authenticationServerSelectionLoginTitle : VectorL10n.authenticationServerSelectionRegisterTitle
flow == .login ? BWIL10n.authenticationServerSelectionLoginTitle : VectorL10n.authenticationServerSelectionRegisterTitle
}
var headerMessage: String {
@@ -116,6 +116,17 @@ final class AuthenticationServerSelectionCoordinator: Coordinator, Presentable {
let homeserverAddress = HomeserverAddress.sanitized(homeserverAddress)
if BWIBuildSettings.shared.bwiEnableLoginProtection {
let protectionService = LoginProtectionService()
protectionService.hashes = BWIBuildSettings.shared.bwiHashes
guard protectionService.isValid(homeserverAddress) else {
stopLoading()
authenticationServerSelectionViewModel.displayError(.footerMessage(BWIL10n.bwiLoginProtectionErrorMessage(AppInfo.current.displayName)))
return
}
}
Task {
do {
try await authenticationService.startFlow(parameters.flow, for: homeserverAddress)
@@ -58,7 +58,7 @@ struct AuthenticationServerSelectionScreen: View {
/// The title, message and icon at the top of the screen.
var header: some View {
VStack(spacing: 8) {
OnboardingIconImage(image: Asset.Images.authenticationServerSelectionIcon)
OnboardingIconImage(image: Asset.Images.welcomeExperience1)
.padding(.bottom, 8)
Text(viewModel.viewState.headerTitle)
@@ -24,8 +24,8 @@ struct OnboardingIconImage: View {
var body: some View {
Image(image.name)
.resizable()
.renderingMode(.template)
.foregroundColor(theme.colors.accent)
//.renderingMode(.template)
//.foregroundColor(theme.colors.accent)
.frame(width: OnboardingMetrics.iconSize, height: OnboardingMetrics.iconSize)
.background(Circle().foregroundColor(.white).padding(2))
.accessibilityHidden(true)
@@ -3,20 +3,24 @@ import SwiftUI
/// Helper class for making our SwiftUI view available to ObjectiveC
@objcMembers class WelcomeExperienceViewController: NSObject {
class func makeViewController(session: MXSession) -> UIViewController {
return UIHostingController(rootView: WelcomeExperienceView())
class func makeViewController(completion: (() -> Void)?) -> UIViewController {
let welcomeExperienceView = WelcomeExperienceView(completion: completion)
return UIHostingController(rootView: welcomeExperienceView)
}
}
struct WelcomeExperienceView: View {
@State var completion: (() -> Void)?
var body: some View {
TabView {
Page(image: "welcome_experience_1", title: NSLocalizedString("welcome_experience_title1", comment: ""), description: NSLocalizedString("welcome_experience_title1", comment: ""))
Page(image: "welcome_experience_2", title: NSLocalizedString("welcome_experience_title2", comment: ""), description: NSLocalizedString("welcome_experience_title2", comment: ""))
Page(image: "welcome_experience_3", title: NSLocalizedString("welcome_experience_title3", comment: ""), description: NSLocalizedString("welcome_experience_title3", comment: ""))
Page(image: "welcome_experience_4", title: NSLocalizedString("welcome_experience_title4", comment: ""), description: NSLocalizedString("welcome_experience_title4", comment: ""))
Page(image: "welcome_experience_5", title: NSLocalizedString("welcome_experience_title5", comment: ""), description: NSLocalizedString("welcome_experience_title5", comment: ""))
Page(image: "welcome_experience_2", title: NSLocalizedString("welcome_experience_title2", comment: ""), description: NSLocalizedString("welcome_experience_title2", comment: ""), doneButton: NSLocalizedString("welcome_experience_start", comment: ""))
Page(image: "welcome_experience_1", title: BWIL10n.welcomeExperienceTitle1, description: BWIL10n.welcomeExperienceDescription1)
Page(image: "welcome_experience_2", title: BWIL10n.welcomeExperienceTitle2, description: BWIL10n.welcomeExperienceDescription2)
Page(image: "welcome_experience_3", title: BWIL10n.welcomeExperienceTitle3, description: BWIL10n.welcomeExperienceDescription3)
Page(image: "welcome_experience_4", title: BWIL10n.welcomeExperienceTitle4, description: BWIL10n.welcomeExperienceDescription4)
Page(image: "welcome_experience_5", title: BWIL10n.welcomeExperienceTitle5, description: BWIL10n.welcomeExperienceDescription5, doneButton: BWIL10n.welcomeExperienceStart, buttonCallback: completion)
}
.tabViewStyle(.page)
.indexViewStyle(.page(backgroundDisplayMode: .always))
@@ -30,13 +34,15 @@ fileprivate struct Page: View {
var description: String
var doneButton: String?
@State var buttonCallback: (() -> Void)?
var body: some View {
GeometryReader { geometry in
VStack(alignment: .center, spacing: 50) {
VStack(alignment: .center, spacing: 40) {
Image(image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: geometry.size.height * 0.3)
.frame(width: geometry.size.height * 0.25)
Text(title)
.foregroundColor(.black)
@@ -48,8 +54,8 @@ fileprivate struct Page: View {
.multilineTextAlignment(.center)
Spacer()
if let doneButton = doneButton {
Button(action: {}) {
if let doneButton = doneButton, let buttonCallback = buttonCallback {
Button(action: buttonCallback) {
Text(doneButton)
.foregroundColor(.white)
.padding(.vertical, 10)
@@ -59,7 +65,6 @@ fileprivate struct Page: View {
}
.padding(.bottom, 100)
}
}
.padding(.top, 100)
.padding(.horizontal, 20)