Skip to content

Commit

Permalink
Merge branch 'release/1.11.23/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
pixlwave committed Jan 10, 2025
2 parents c12e1f3 + e600cc9 commit 6ed7088
Show file tree
Hide file tree
Showing 33 changed files with 508 additions and 62 deletions.
10 changes: 10 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## Changes in 1.11.23 (2025-01-10)

🙌 Improvements

- Show sunset banners, guiding users to install Element X when registering a new account against a server with MAS. ([#7889](https://github.com/element-hq/element-ios/issues/7889))

⚠️ API Changes

- Adds a BuildSettings.replacementApp setting, used to control the sunset banners. ([#7889](https://github.com/element-hq/element-ios/issues/7889))

## Changes in 1.11.22 (2024-12-10)

🐛 Bugfixes
Expand Down
4 changes: 2 additions & 2 deletions Config/AppVersion.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@
//

// Version
MARKETING_VERSION = 1.11.22
CURRENT_PROJECT_VERSION = 1.11.22
MARKETING_VERSION = 1.11.23
CURRENT_PROJECT_VERSION = 1.11.23
21 changes: 21 additions & 0 deletions Config/BuildSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -441,4 +441,25 @@ final class BuildSettings: NSObject {

// MARK: - Alerts
static let showUnverifiedSessionsAlert = true

// MARK: - Sunset

/// Meta data about the app that will replaces this one with Matrix 2.0 support.
struct ReplacementApp {
/// The app's display name, used in marketing banners.
let name = "Element X"
/// A link that will be opened to tell the user more about the new app, Matrix 2.0 and the migration.
let learnMoreURL = URL(string: "https://element.io/app-for-productivity")!
/// The app's iTunes/product ID, used to show the App Store page in-app.
let productID = "1631335820"
/// A fallback URL that will be opened if there are any issues showing the App Store page in-app.
let appStoreURL = URL(string: "https://apps.apple.com/app/element-x-secure-chat-call/id1631335820")!
}

/// Information about the Matrix 2.0 compatible app that will replace this one in the future.
///
/// The presence of this setting acts as a feature flag to show marketing banners for the app
/// when it is detected that the homeserver is running Matrix 2.0. Set this to `nil` until you
/// are ready to migrate your users.
static let replacementApp: ReplacementApp? = .init()
}
10 changes: 5 additions & 5 deletions Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,16 @@ PODS:
- LoggerAPI (1.9.200):
- Logging (~> 1.1)
- Logging (1.4.0)
- MatrixSDK (0.27.16):
- MatrixSDK/Core (= 0.27.16)
- MatrixSDK/Core (0.27.16):
- MatrixSDK (0.27.17):
- MatrixSDK/Core (= 0.27.17)
- MatrixSDK/Core (0.27.17):
- AFNetworking (~> 4.0.0)
- GZIP (~> 1.3.0)
- libbase58 (~> 0.1.4)
- MatrixSDKCrypto (= 0.4.3)
- Realm (= 10.27.0)
- SwiftyBeaver (= 1.9.5)
- MatrixSDK/JingleCallStack (0.27.16):
- MatrixSDK/JingleCallStack (0.27.17):
- JitsiMeetSDKLite (= 8.1.2-lite)
- MatrixSDK/Core
- MatrixSDKCrypto (0.4.3)
Expand Down Expand Up @@ -179,7 +179,7 @@ SPEC CHECKSUMS:
libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75
LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d
Logging: beeb016c9c80cf77042d62e83495816847ef108b
MatrixSDK: ce8f2cec670c2212144a129cc617d4144c89b97f
MatrixSDK: e3096b0b47f8a0bde6ae3f614f9c49e7e92b03ea
MatrixSDKCrypto: 27bee960e0e8b3a3039f3f3e93dd2ec88299c77e
ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d
Realm: 9ca328bd7e700cc19703799785e37f77d1a130f2
Expand Down
6 changes: 6 additions & 0 deletions Riot/Assets/Images.xcassets/Sunset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "ElementXBannerIcon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "[email protected]",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "[email protected]",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions Riot/Assets/en.lproj/Vector.strings
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@
"accessibility_button_label" = "button";
"accessibility_selected" = "selected";

// MARK: Sunset
"sunset_delegated_oidc_registration_not_supported_title" = "You can no longer create an account with %1$@ using this app";
"sunset_delegated_oidc_registration_not_supported_message" = "Download %1$@ to use %2$@ for your account or choose a different homeserver.";
"sunset_delegated_oidc_registration_not_supported_generic_error" = "You can no longer create an account with the entered homeserver using this app";
"sunset_download_banner_title" = "Download %1$@";
"sunset_download_banner_message" = "Faster, more secure, and packed with powerful collaboration tools.";
"sunset_download_banner_learn_more" = "Learn more";

// MARK: Onboarding
"onboarding_splash_register_button_title" = "Create account";
"onboarding_splash_login_button_title" = "I already have an account";
Expand Down
1 change: 1 addition & 0 deletions Riot/Generated/Images.swift
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ internal class Asset: NSObject {
internal static let voiceBroadcastTileMic = ImageAsset(name: "voice_broadcast_tile_mic")
internal static let voiceBroadcastTimeLeft = ImageAsset(name: "voice_broadcast_time_left")
internal static let launchScreenLogo = ImageAsset(name: "launch_screen_logo")
internal static let sunsetBannerIcon = ImageAsset(name: "sunset_banner_icon")
}
@objcMembers
@objc(AssetSharedImages) internal class SharedImages: NSObject {
Expand Down
24 changes: 24 additions & 0 deletions Riot/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8743,6 +8743,30 @@ public class VectorL10n: NSObject {
public static var suggest: String {
return VectorL10n.tr("Vector", "suggest")
}
/// You can no longer create an account with the entered homeserver using this app
public static var sunsetDelegatedOidcRegistrationNotSupportedGenericError: String {
return VectorL10n.tr("Vector", "sunset_delegated_oidc_registration_not_supported_generic_error")
}
/// Download %1$@ to use %2$@ for your account or choose a different homeserver.
public static func sunsetDelegatedOidcRegistrationNotSupportedMessage(_ p1: String, _ p2: String) -> String {
return VectorL10n.tr("Vector", "sunset_delegated_oidc_registration_not_supported_message", p1, p2)
}
/// You can no longer create an account with %1$@ using this app
public static func sunsetDelegatedOidcRegistrationNotSupportedTitle(_ p1: String) -> String {
return VectorL10n.tr("Vector", "sunset_delegated_oidc_registration_not_supported_title", p1)
}
/// Learn more
public static var sunsetDownloadBannerLearnMore: String {
return VectorL10n.tr("Vector", "sunset_download_banner_learn_more")
}
/// Faster, more secure, and packed with powerful collaboration tools.
public static var sunsetDownloadBannerMessage: String {
return VectorL10n.tr("Vector", "sunset_download_banner_message")
}
/// Download %1$@
public static func sunsetDownloadBannerTitle(_ p1: String) -> String {
return VectorL10n.tr("Vector", "sunset_download_banner_title", p1)
}
/// Switch
public static var `switch`: String {
return VectorL10n.tr("Vector", "switch")
Expand Down
11 changes: 7 additions & 4 deletions Riot/Modules/Authentication/AuthenticationCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,13 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
return
}

var showReplacementAppBanner = false
do {
// Start the flow (if homeserverAddress is nil, the default server will be used).
try await authenticationService.startFlow(flow)
} catch RegistrationError.delegatedOIDCRequiresReplacementApp where BuildSettings.replacementApp != nil {
// The flow can continue, allowing the Registration Screen to display the banner.
showReplacementAppBanner = true
} catch {
MXLog.error("[AuthenticationCoordinator] start: Failed to start, showing server selection.")
showServerSelectionScreen(for: flow)
Expand All @@ -151,7 +155,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
if authenticationService.state.homeserver.needsRegistrationFallback {
showFallback(for: flow)
} else {
showRegistrationScreen()
showRegistrationScreen(showReplacementAppBanner: showReplacementAppBanner)
}
case .login:
if authenticationService.state.homeserver.needsLoginFallback {
Expand Down Expand Up @@ -354,13 +358,12 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
// MARK: - Registration

/// Shows the registration screen.
@MainActor private func showRegistrationScreen() {
@MainActor private func showRegistrationScreen(showReplacementAppBanner: Bool = false) {
MXLog.debug("[AuthenticationCoordinator] showRegistrationScreen")
let homeserver = authenticationService.state.homeserver
let parameters = AuthenticationRegistrationCoordinatorParameters(navigationRouter: navigationRouter,
authenticationService: authenticationService,
registrationFlow: homeserver.registrationFlow,
loginMode: homeserver.preferredLoginMode)
showReplacementAppBanner: showReplacementAppBanner)
let coordinator = AuthenticationRegistrationCoordinator(parameters: parameters)
coordinator.callback = { [weak self, weak coordinator] result in
guard let self = self, let coordinator = coordinator else { return }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ enum AuthenticationError: String, LocalizedError {
/// Errors that can be thrown from `RegistrationWizard`
enum RegistrationError: String, LocalizedError {
case registrationDisabled
case delegatedOIDCRequiresReplacementApp
case createAccountNotCalled
case missingThreePIDData
case missingThreePIDURL
Expand All @@ -58,6 +59,8 @@ enum RegistrationError: String, LocalizedError {
return VectorL10n.authMsisdnValidationError
case .invalidPhoneNumber:
return VectorL10n.authenticationVerifyMsisdnInvalidPhoneNumber
case .delegatedOIDCRequiresReplacementApp:
return VectorL10n.sunsetDelegatedOidcRegistrationNotSupportedGenericError
default:
return VectorL10n.errorCommonMessage
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,13 @@ class AuthenticationService: NSObject {
self.registrationWizard = registrationWizard
} catch {
guard homeserver.preferredLoginMode.hasSSO, error as? RegistrationError == .registrationDisabled else {
throw error
if homeserver.preferredLoginMode.providesDelegatedOIDCCompatibility {
throw RegistrationError.delegatedOIDCRequiresReplacementApp
} else {
throw error
}
}

// Continue without throwing when registration is disabled but SSO is available.
}
}
Expand Down Expand Up @@ -281,10 +286,13 @@ class AuthenticationService: NSObject {
// Get the login flow
let loginFlowResponse = try await client.getLoginSession()

let identityProviders = loginFlowResponse.flows?.compactMap { $0 as? MXLoginSSOFlow }.first?.identityProviders ?? []
let firstSSOFlow = loginFlowResponse.flows?.compactMap { $0 as? MXLoginSSOFlow }.first
let identityProviders = firstSSOFlow?.identityProviders ?? []
let providesDelegatedOIDCCompatibility = firstSSOFlow?.delegatedOIDCCompatibility ?? false
return LoginFlowResult(supportedLoginTypes: loginFlowResponse.flows?.compactMap { $0 } ?? [],
ssoIdentityProviders: identityProviders.sorted { $0.name < $1.name }.map(\.ssoIdentityProvider),
homeserverAddress: client.homeserver)
homeserverAddress: client.homeserver,
providesDelegatedOIDCCompatibility: providesDelegatedOIDCCompatibility)
}

/// Perform a well-known request on the specified homeserver URL.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ struct LoginFlowResult {
let supportedLoginTypes: [MXLoginFlow]
let ssoIdentityProviders: [SSOIdentityProvider]
let homeserverAddress: String
let providesDelegatedOIDCCompatibility: Bool

var loginMode: LoginMode {
if supportedLoginTypes.contains(where: { $0.type == kMXLoginFlowTypeSSO }),
supportedLoginTypes.contains(where: { $0.type == kMXLoginFlowTypePassword }) {
supportedLoginTypes.contains(where: { $0.type == kMXLoginFlowTypePassword }),
!providesDelegatedOIDCCompatibility {
return .ssoAndPassword(ssoIdentityProviders: ssoIdentityProviders)
} else if supportedLoginTypes.contains(where: { $0.type == kMXLoginFlowTypeSSO }) {
return .sso(ssoIdentityProviders: ssoIdentityProviders)
return .sso(ssoIdentityProviders: ssoIdentityProviders, providesDelegatedOIDCCompatibility: providesDelegatedOIDCCompatibility)
} else if supportedLoginTypes.contains(where: { $0.type == kMXLoginFlowTypePassword }) {
return .password
} else {
Expand All @@ -34,15 +36,15 @@ enum LoginMode {
/// The homeserver supports login with a password.
case password
/// The homeserver supports login via one or more SSO providers.
case sso(ssoIdentityProviders: [SSOIdentityProvider])
case sso(ssoIdentityProviders: [SSOIdentityProvider], providesDelegatedOIDCCompatibility: Bool)
/// The homeserver supports login with either a password or via an SSO provider.
case ssoAndPassword(ssoIdentityProviders: [SSOIdentityProvider])
/// The homeserver only allows login with unsupported mechanisms. Use fallback instead.
case unsupported

var ssoIdentityProviders: [SSOIdentityProvider]? {
switch self {
case .sso(let ssoIdentityProviders), .ssoAndPassword(let ssoIdentityProviders):
case .sso(let ssoIdentityProviders, _), .ssoAndPassword(let ssoIdentityProviders):
// Provide a backup for homeservers that support SSO but don't offer any identity providers
// https://spec.matrix.org/latest/client-server-api/#client-login-via-sso
return ssoIdentityProviders.count > 0 ? ssoIdentityProviders : [SSOIdentityProvider(id: "", name: "SSO", brand: nil, iconURL: nil)]
Expand All @@ -60,6 +62,15 @@ enum LoginMode {
}
}

var providesDelegatedOIDCCompatibility: Bool {
switch self {
case .sso(_, providesDelegatedOIDCCompatibility: true):
return true
default:
return false
}
}

var supportsPasswordFlow: Bool {
switch self {
case .password, .ssoAndPassword:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ enum AuthenticationRegistrationViewModelResult: CustomStringConvertible {
case continueWithSSO(SSOIdentityProvider)
/// Continue using a fallback
case fallback
/// Show the app store page for the replacement app.
case downloadReplacementApp(BuildSettings.ReplacementApp)

/// A string representation of the result, ignoring any associated values that could leak PII.
var description: String {
Expand All @@ -34,6 +36,8 @@ enum AuthenticationRegistrationViewModelResult: CustomStringConvertible {
return "continueWithSSO: \(provider)"
case .fallback:
return "fallback"
case .downloadReplacementApp:
return "downloadReplacementApp"
}
}
}
Expand All @@ -52,6 +56,8 @@ struct AuthenticationRegistrationViewState: BindableState {

/// Data about the selected homeserver.
var homeserver: AuthenticationHomeserverViewData

var showReplacementAppBanner: Bool
/// Whether a new homeserver is currently being loaded.
var isLoading = false
/// View state that can be bound to from SwiftUI.
Expand Down Expand Up @@ -85,7 +91,7 @@ struct AuthenticationRegistrationViewState: BindableState {

/// Whether to show any SSO buttons.
var showSSOButtons: Bool {
!homeserver.ssoIdentityProviders.isEmpty
!homeserver.ssoIdentityProviders.isEmpty && !showReplacementAppBanner
}

/// Whether the current `username` is invalid.
Expand Down Expand Up @@ -137,6 +143,8 @@ enum AuthenticationRegistrationViewAction {
case continueWithSSO(SSOIdentityProvider)
/// Continue using the fallback page
case fallback
/// Show the app store page for the replacement app.
case downloadReplacementApp(BuildSettings.ReplacementApp)
}

enum AuthenticationRegistrationErrorType: Hashable {
Expand All @@ -151,6 +159,8 @@ enum AuthenticationRegistrationErrorType: Hashable {
case invalidResponse
/// The homeserver doesn't support registration.
case registrationDisabled
/// The app doesn't support registration with this homeserver.
case registrationNotSupported
/// An unknown error occurred.
case unknown
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ class AuthenticationRegistrationViewModel: AuthenticationRegistrationViewModelTy

// MARK: - Setup

init(homeserver: AuthenticationHomeserverViewData) {
init(homeserver: AuthenticationHomeserverViewData, showReplacementAppBanner: Bool) {
let bindings = AuthenticationRegistrationBindings()
let viewState = AuthenticationRegistrationViewState(homeserver: homeserver, bindings: bindings)
let viewState = AuthenticationRegistrationViewState(homeserver: homeserver, showReplacementAppBanner: showReplacementAppBanner, bindings: bindings)

super.init(initialViewState: viewState)
}
Expand All @@ -44,6 +44,8 @@ class AuthenticationRegistrationViewModel: AuthenticationRegistrationViewModelTy
Task { await callback?(.continueWithSSO(provider)) }
case .fallback:
Task { await callback?(.fallback) }
case .downloadReplacementApp(let replacementApp):
Task { await callback?(.downloadReplacementApp(replacementApp)) }
}
}

Expand All @@ -54,6 +56,10 @@ class AuthenticationRegistrationViewModel: AuthenticationRegistrationViewModelTy

@MainActor func update(homeserver: AuthenticationHomeserverViewData) {
state.homeserver = homeserver

// Only the initial homeserver will ever need this, it isn't possible to update the
// server to another one that requires the replacement app as the selection will fail.
state.showReplacementAppBanner = false
}

@MainActor func update(username: String) {
Expand Down Expand Up @@ -82,6 +88,10 @@ class AuthenticationRegistrationViewModel: AuthenticationRegistrationViewModelTy
state.bindings.alertInfo = AlertInfo(id: type,
title: VectorL10n.error,
message: VectorL10n.loginErrorRegistrationIsNotSupported)
case .registrationNotSupported:
state.bindings.alertInfo = AlertInfo(id: type,
title: VectorL10n.error,
message: VectorL10n.sunsetDelegatedOidcRegistrationNotSupportedGenericError)
case .unknown:
state.bindings.alertInfo = AlertInfo(id: type)
}
Expand Down
Loading

0 comments on commit 6ed7088

Please sign in to comment.