Skip to content

Commit

Permalink
QR Code Login Flow (#2767)
Browse files Browse the repository at this point in the history
Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>
Co-authored-by: Hugh Nimmo-Smith <hughns@element.io>
  • Loading branch information
3 people authored May 29, 2024
1 parent 66a8f6f commit 4e23f7b
Show file tree
Hide file tree
Showing 72 changed files with 1,236 additions and 221 deletions.
68 changes: 36 additions & 32 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/matrix-rust-components-swift",
"state" : {
"revision" : "100598671d3e6186f77e2f48630e9cbcb63fd86b",
"version" : "1.0.4"
"revision" : "510689e69b36de3c6bb8313b92f2709e58d2e80f",
"version" : "1.0.6"
}
},
{
Expand Down
12 changes: 12 additions & 0 deletions ElementX/Resources/Localizations/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,18 @@
"screen_qr_code_login_connection_note_secure_state_title" = "Connection not secure";
"screen_qr_code_login_device_code_subtitle" = "You’ll be asked to enter the two digits shown on this device.";
"screen_qr_code_login_device_code_title" = "Enter the number below on your other device";
"screen_qr_code_login_device_not_signed_in_scan_state_description" = "Sign in to your other device and then try again, or use another device that’s already signed in.";
"screen_qr_code_login_device_not_signed_in_scan_state_subtitle" = "Other device not signed in";
"screen_qr_code_login_error_cancelled_subtitle" = "The sign in was cancelled on the other device.";
"screen_qr_code_login_error_cancelled_title" = "Sign in request cancelled";
"screen_qr_code_login_error_declined_subtitle" = "The request on your other device was not accepted.";
"screen_qr_code_login_error_declined_title" = "Sign in declined";
"screen_qr_code_login_error_expired_subtitle" = "Sign in expired. Please try again.";
"screen_qr_code_login_error_expired_title" = "The sign in was not completed in time";
"screen_qr_code_login_error_linking_not_suported_subtitle" = "Your other device does not support signing in to %@ with a QR code.\n\nTry signing in manually, or scan the QR code with another device.";
"screen_qr_code_login_error_linking_not_suported_title" = "QR code not supported";
"screen_qr_code_login_error_sliding_sync_not_supported_subtitle" = "Your account provider does not support %1$@.";
"screen_qr_code_login_error_sliding_sync_not_supported_title" = "%1$@ not supported";
"screen_qr_code_login_initial_state_button_title" = "Ready to scan";
"screen_qr_code_login_initial_state_item_1" = "Open %1$@ on a desktop device";
"screen_qr_code_login_initial_state_item_2" = "Click on your avatar";
Expand Down
8 changes: 7 additions & 1 deletion ElementX/Sources/Application/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -444,10 +444,16 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
}

private func startAuthentication() {
let encryptionKeyProvider = EncryptionKeyProvider()
let authenticationService = AuthenticationServiceProxy(userSessionStore: userSessionStore,
encryptionKeyProvider: EncryptionKeyProvider(),
encryptionKeyProvider: encryptionKeyProvider,
appSettings: appSettings)
let qrCodeLoginService = QRCodeLoginService(oidcConfiguration: appSettings.oidcConfiguration.rustValue,
encryptionKeyProvider: encryptionKeyProvider,
userSessionStore: userSessionStore)

authenticationFlowCoordinator = AuthenticationFlowCoordinator(authenticationService: authenticationService,
qrCodeLoginService: qrCodeLoginService,
bugReportService: ServiceLocator.shared.bugReportService,
navigationRootCoordinator: navigationRootCoordinator,
appMediator: appMediator,
Expand Down
19 changes: 19 additions & 0 deletions ElementX/Sources/Application/AppMediator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// limitations under the License.
//

import AVFoundation
import UIKit

class AppMediator: AppMediatorProtocol {
Expand Down Expand Up @@ -64,4 +65,22 @@ class AppMediator: AppMediatorProtocol {
func setIdleTimerDisabled(_ disabled: Bool) {
application.isIdleTimerDisabled = disabled
}

func requestAuthorizationIfNeeded() async -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: .video)

// Determine if the user previously authorized camera access.
if status == .authorized {
return true
}

var isAuthorized = false
// If the system hasn't determined the user's authorization status,
// explicitly prompt them for approval.
if status == .notDetermined {
isAuthorized = await AVCaptureDevice.requestAccess(for: .video)
}

return isAuthorized
}
}
2 changes: 2 additions & 0 deletions ElementX/Sources/Application/AppMediatorProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ protocol AppMediatorProtocol {
func openAppSettings()

func setIdleTimerDisabled(_ disabled: Bool)

func requestAuthorizationIfNeeded() async -> Bool
}

extension UIApplication.State: CustomStringConvertible {
Expand Down
9 changes: 9 additions & 0 deletions ElementX/Sources/Application/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,15 @@ final class AppSettings {
return url
}()

private(set) lazy var oidcConfiguration = OIDCConfigurationProxy(clientName: InfoPlistReader.main.bundleDisplayName,
redirectURI: oidcRedirectURL,
clientURI: websiteURL,
logoURI: logoURL,
tosURI: acceptableUseURL,
policyURI: privacyURL,
contacts: [supportEmailAddress],
staticRegistrations: oidcStaticRegistrations.mapKeys { $0.absoluteString })

/// A dictionary of accounts that have performed an initial sync through their proxy.
///
/// This is a temporary workaround. In the future we should be able to receive a signal from the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
private let appSettings: AppSettings
private let analytics: AnalyticsService
private let userIndicatorController: UserIndicatorControllerProtocol
private let qrCodeLoginService: QRCodeLoginServiceProtocol

private var cancellables = Set<AnyCancellable>()

Expand All @@ -42,6 +43,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
weak var delegate: AuthenticationFlowCoordinatorDelegate?

init(authenticationService: AuthenticationServiceProxyProtocol,
qrCodeLoginService: QRCodeLoginServiceProtocol,
bugReportService: BugReportServiceProtocol,
navigationRootCoordinator: NavigationRootCoordinator,
appMediator: AppMediatorProtocol,
Expand All @@ -55,6 +57,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
self.appSettings = appSettings
self.analytics = analytics
self.userIndicatorController = userIndicatorController
self.qrCodeLoginService = qrCodeLoginService

navigationStackCoordinator = NavigationStackCoordinator()
}
Expand Down Expand Up @@ -106,16 +109,26 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
}

private func startQRCodeLogin() {
let coordinator = QRCodeLoginScreenCoordinator(parameters: .init(qrCodeLoginService: QRCodeLoginService(),
let coordinator = QRCodeLoginScreenCoordinator(parameters: .init(qrCodeLoginService: qrCodeLoginService,
orientationManager: appMediator.windowManager,
appMediator: appMediator))
coordinator.actionsPublisher.sink { [weak self] action in
guard let self else {
return
}
switch action {
case .signInManually:
navigationStackCoordinator.setSheetCoordinator(nil)
Task { await self.startAuthentication() }
case .cancel:
navigationStackCoordinator.setSheetCoordinator(nil)
case .done(let userSession):
navigationStackCoordinator.setSheetCoordinator(nil)
// Since the qr code login flow includes verification
appSettings.hasRunIdentityConfirmationOnboarding = true
DispatchQueue.main.async {
self.userHasSignedIn(userSession: userSession)
}
}
}
.store(in: &cancellables)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,9 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {

configureStateMachine()

stateMachine.tryEvent(.next)

rootNavigationStackCoordinator.setFullScreenCoverCoordinator(navigationStackCoordinator, animated: !isNewLogin)

stateMachine.tryEvent(.next)
}

func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
Expand Down Expand Up @@ -134,6 +134,8 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol {
return .analyticsPrompt
case (.initial, false, false, false, true):
return .notificationPermissions
case (.initial, false, false, false, false):
return .finished

case (.identityConfirmation, _, _, _, _):
return .identityConfirmed
Expand Down
32 changes: 32 additions & 0 deletions ElementX/Sources/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1221,6 +1221,38 @@ internal enum L10n {
internal static var screenQrCodeLoginDeviceCodeSubtitle: String { return L10n.tr("Localizable", "screen_qr_code_login_device_code_subtitle") }
/// Enter the number below on your other device
internal static var screenQrCodeLoginDeviceCodeTitle: String { return L10n.tr("Localizable", "screen_qr_code_login_device_code_title") }
/// Sign in to your other device and then try again, or use another device that’s already signed in.
internal static var screenQrCodeLoginDeviceNotSignedInScanStateDescription: String { return L10n.tr("Localizable", "screen_qr_code_login_device_not_signed_in_scan_state_description") }
/// Other device not signed in
internal static var screenQrCodeLoginDeviceNotSignedInScanStateSubtitle: String { return L10n.tr("Localizable", "screen_qr_code_login_device_not_signed_in_scan_state_subtitle") }
/// The sign in was cancelled on the other device.
internal static var screenQrCodeLoginErrorCancelledSubtitle: String { return L10n.tr("Localizable", "screen_qr_code_login_error_cancelled_subtitle") }
/// Sign in request cancelled
internal static var screenQrCodeLoginErrorCancelledTitle: String { return L10n.tr("Localizable", "screen_qr_code_login_error_cancelled_title") }
/// The request on your other device was not accepted.
internal static var screenQrCodeLoginErrorDeclinedSubtitle: String { return L10n.tr("Localizable", "screen_qr_code_login_error_declined_subtitle") }
/// Sign in declined
internal static var screenQrCodeLoginErrorDeclinedTitle: String { return L10n.tr("Localizable", "screen_qr_code_login_error_declined_title") }
/// Sign in expired. Please try again.
internal static var screenQrCodeLoginErrorExpiredSubtitle: String { return L10n.tr("Localizable", "screen_qr_code_login_error_expired_subtitle") }
/// The sign in was not completed in time
internal static var screenQrCodeLoginErrorExpiredTitle: String { return L10n.tr("Localizable", "screen_qr_code_login_error_expired_title") }
/// Your other device does not support signing in to %@ with a QR code.
///
/// Try signing in manually, or scan the QR code with another device.
internal static func screenQrCodeLoginErrorLinkingNotSuportedSubtitle(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_qr_code_login_error_linking_not_suported_subtitle", String(describing: p1))
}
/// QR code not supported
internal static var screenQrCodeLoginErrorLinkingNotSuportedTitle: String { return L10n.tr("Localizable", "screen_qr_code_login_error_linking_not_suported_title") }
/// Your account provider does not support %1$@.
internal static func screenQrCodeLoginErrorSlidingSyncNotSupportedSubtitle(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_qr_code_login_error_sliding_sync_not_supported_subtitle", String(describing: p1))
}
/// %1$@ not supported
internal static func screenQrCodeLoginErrorSlidingSyncNotSupportedTitle(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_qr_code_login_error_sliding_sync_not_supported_title", String(describing: p1))
}
/// Ready to scan
internal static var screenQrCodeLoginInitialStateButtonTitle: String { return L10n.tr("Localizable", "screen_qr_code_login_initial_state_button_title") }
/// Open %1$@ on a desktop device
Expand Down
3 changes: 2 additions & 1 deletion ElementX/Sources/Mocks/AppMediatorMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@
import UIKit

extension AppMediatorMock {
static var `default`: AppMediatorProtocol {
static var `default`: AppMediatorMock {
let mock = AppMediatorMock()

mock.underlyingAppState = .active
mock.requestAuthorizationIfNeededUnderlyingReturnValue = true
mock.underlyingWindowManager = WindowManagerMock()

return mock
Expand Down
Loading

0 comments on commit 4e23f7b

Please sign in to comment.