-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bug# 6733: refactor: cleanup and simplify logic flow #6895
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,8 +3,8 @@ | |
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
||
import WebKit | ||
import UIKit | ||
import Account | ||
import Shared | ||
|
||
enum DismissType { | ||
case dismiss | ||
|
@@ -31,21 +31,13 @@ class FxAWebViewController: UIViewController, WKNavigationDelegate { | |
- parameter deepLinkParams: URL args passed in from deep link that propagate to FxA web view | ||
*/ | ||
init(pageType: FxAPageType, profile: Profile, dismissalStyle: DismissType, deepLinkParams: FxALaunchParams?) { | ||
self.viewModel = FxAWebViewModel( | ||
pageType: pageType, | ||
profile: profile, | ||
deepLinkParams: deepLinkParams, | ||
firefoxAccounts: RustFirefoxAccounts.shared, | ||
leanPlumClient: LeanPlumClient.shared | ||
) | ||
self.viewModel = FxAWebViewModel(pageType: pageType, profile: profile, deepLinkParams: deepLinkParams) | ||
|
||
self.dismissType = dismissalStyle | ||
|
||
let contentController = WKUserContentController() | ||
|
||
if let userScript = FxAWebViewModel.makeSignInUserScript() { | ||
contentController.addUserScript(userScript) | ||
} | ||
viewModel.setupUserScript(for: contentController) | ||
|
||
let config = WKWebViewConfiguration() | ||
config.userContentController = contentController | ||
webView = WKWebView(frame: .zero, configuration: config) | ||
|
@@ -73,11 +65,8 @@ class FxAWebViewController: UIViewController, WKNavigationDelegate { | |
webView.navigationDelegate = self | ||
view = webView | ||
|
||
viewModel.authenticate() | ||
|
||
viewModel.onEmittingNewState = { [weak self] output in | ||
let (request, method) = output | ||
if let method = method { | ||
viewModel.setupFirstPage { [weak self] (request, telemetryEventMethod) in | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Notes: |
||
if let method = telemetryEventMethod { | ||
UnifiedTelemetry.recordEvent(category: .firefoxAccount, method: method, object: .accountConnected) | ||
} | ||
self?.webView.load(request) | ||
|
@@ -86,10 +75,6 @@ class FxAWebViewController: UIViewController, WKNavigationDelegate { | |
viewModel.onDismissController = { [weak self] in | ||
self?.dismiss(animated: true) | ||
} | ||
|
||
viewModel.onWantingToExecuteJSScript = { [weak self] msg in | ||
self?.webView.evaluateJavaScript(msg) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: confusing to bounce back here for this one-line |
||
} | ||
|
||
/** | ||
|
@@ -111,15 +96,16 @@ class FxAWebViewController: UIViewController, WKNavigationDelegate { | |
} | ||
|
||
extension FxAWebViewController: WKScriptMessageHandler { | ||
|
||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { | ||
viewModel.parseAndExecuteSuitableRemoteCommand(basedOn: message) | ||
viewModel.handle(scriptMessage: message) | ||
} | ||
} | ||
|
||
extension FxAWebViewController { | ||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { | ||
viewModel.dispatchLongPressCommand() | ||
let hideLongpress = "document.body.style.webkitTouchCallout='none';" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no model logic inherent in doing this, do it here. |
||
webView.evaluateJavaScript(hideLongpress) | ||
|
||
//The helpBrowser shows the current URL in the navbar, the main fxa webview does not. | ||
guard webView !== helpBrowser else { | ||
navigationItem.title = viewModel.composeTitle(basedOn: webView.url, hasOnlySecureContent: webView.hasOnlySecureContent) | ||
|
@@ -144,7 +130,7 @@ extension FxAWebViewController: WKUIDelegate { | |
helpBrowser = wv | ||
helpBrowser?.navigationDelegate = self | ||
|
||
navigationItem.leftBarButtonItem = UIBarButtonItem(title: FxAWebViewModel.BackTitle, style: .plain, target: self, action: #selector(closeHelpBrowser)) | ||
navigationItem.leftBarButtonItem = UIBarButtonItem(title: Strings.BackTitle, style: .plain, target: self, action: #selector(closeHelpBrowser)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: capitalization was wrong on FxAWebViewModel.BackTitle, but also the helpviewer logic is already here, no reason to jump there for this. |
||
|
||
return helpBrowser | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,17 +9,6 @@ import MozillaAppServices | |
import Shared | ||
import SwiftKeychainWrapper | ||
|
||
|
||
extension URL { | ||
var toRequest: URLRequest { | ||
return URLRequest(url: self) | ||
} | ||
/// Figuring out if two urls are the same based on scheme, host and path. | ||
func isEquivilent(to url: URL) -> Bool { | ||
return self.scheme == url.scheme && self.host == url.host && self.path == url.path | ||
} | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note on URL extensions:
|
||
enum FxAPageType { | ||
case emailLoginFlow | ||
case qrCode(url: String) | ||
|
@@ -39,90 +28,60 @@ fileprivate enum RemoteCommand: String { | |
case profileChanged = "profile:change" | ||
} | ||
|
||
/// Represent the state of FxAWebViewModel | ||
typealias FxASTATE = (URLRequest, UnifiedTelemetry.EventMethod?) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: the capitalization on this is wrong, please watch for this in reviews. |
||
|
||
class FxAWebViewModel { | ||
|
||
fileprivate let pageType: FxAPageType | ||
fileprivate let profile: Profile | ||
fileprivate var deepLinkParams: FxALaunchParams? | ||
fileprivate(set) var baseURL: URL? | ||
|
||
fileprivate let firefoxAccounts: RustFirefoxAccounts | ||
fileprivate let leanPlumClient: LeanPlumClient | ||
|
||
// This is not shown full-screen, use mobile UA | ||
static let mobileUserAgent = UserAgent.mobileUserAgent() | ||
|
||
static let FxSignInFilePath = Bundle.main.path(forResource: "FxASignIn", ofType: "js") | ||
|
||
static let BackTitle = Strings.BackTitle | ||
|
||
static func makeSignInUserScript() -> WKUserScript? { | ||
guard let path = FxAWebViewModel.FxSignInFilePath, | ||
let source = try? String(contentsOfFile: path, encoding: .utf8) | ||
else { return nil } | ||
|
||
func setupUserScript(for controller: WKUserContentController) { | ||
guard let path = Bundle.main.path(forResource: "FxASignIn", ofType: "js"), let source = try? String(contentsOfFile: path, encoding: .utf8) else { | ||
assert(false) | ||
return | ||
} | ||
let userScript = WKUserScript(source: source, injectionTime: .atDocumentStart, forMainFrameOnly: true) | ||
return userScript | ||
controller.addUserScript(userScript) | ||
} | ||
|
||
/** | ||
init() FxAWebViewModel. | ||
|
||
- parameter pageType: Specify login flow or settings page if already logged in. | ||
- parameter profile: a Profile. | ||
- parameter firefoxAccounts: RustFirefoxAccounts singleton instance | ||
- parameter leanPlumClient: LeanPlumClient singleton instance | ||
|
||
- parameter deepLinkParams: url parameters that originate from a deep link | ||
*/ | ||
required init( | ||
pageType: FxAPageType, | ||
profile: Profile, | ||
deepLinkParams: FxALaunchParams?, | ||
firefoxAccounts: RustFirefoxAccounts, | ||
leanPlumClient: LeanPlumClient | ||
) { | ||
required init(pageType: FxAPageType, profile: Profile, deepLinkParams: FxALaunchParams?) { | ||
self.pageType = pageType | ||
self.profile = profile | ||
self.deepLinkParams = deepLinkParams | ||
self.firefoxAccounts = firefoxAccounts | ||
self.leanPlumClient = leanPlumClient | ||
|
||
|
||
// If accountMigrationFailed then the app menu has a caution icon, | ||
// and at this point the user has taken sufficient action to clear the caution. | ||
firefoxAccounts.accountMigrationFailed = false | ||
profile.rustFxA.accountMigrationFailed = false | ||
} | ||
|
||
fileprivate(set) var baseURL: URL? | ||
|
||
/// This closure will carry out the new success state created by invoking `authenticate` method within this viewModel. | ||
var onEmittingNewState: ((FxASTATE) -> Void)? | ||
|
||
var onDismissController: (() -> Void)? | ||
var onWantingToExecuteJSScript: ((String) -> Void)? | ||
|
||
func composeTitle(basedOn url: URL?, hasOnlySecureContent: Bool) -> String { | ||
return (hasOnlySecureContent ? "🔒 " : "") + (url?.host ?? "") | ||
} | ||
} | ||
|
||
// MARK: - Authentication | ||
extension FxAWebViewModel { | ||
|
||
func authenticate() { | ||
firefoxAccounts.accountManager.uponQueue(.main) { accountManager in | ||
func setupFirstPage(completion: @escaping ((URLRequest, UnifiedTelemetry.EventMethod?) -> Void)) { | ||
profile.rustFxA.accountManager.uponQueue(.main) { accountManager in | ||
accountManager.getManageAccountURL(entrypoint: "ios_settings_manage") { [weak self] result in | ||
guard let self = self else { return } | ||
|
||
// Handle authentication with either the QR code login flow, email login flow, or settings page flow | ||
switch self.pageType { | ||
case .emailLoginFlow: | ||
accountManager.beginAuthentication { [weak self] result in | ||
guard let self = self else { return } | ||
|
||
if case .success(let url) = result { | ||
self.baseURL = url | ||
self.onEmittingNewState?((self.makeRequest(url), .emailLogin)) | ||
completion(self.makeRequest(url), .emailLogin) | ||
} | ||
} | ||
case let .qrCode(url): | ||
|
@@ -131,13 +90,13 @@ extension FxAWebViewModel { | |
|
||
if case .success(let url) = result { | ||
self.baseURL = url | ||
self.onEmittingNewState?((self.makeRequest(url), .qrPairing)) | ||
completion(self.makeRequest(url), .qrPairing) | ||
} | ||
} | ||
case .settingsPage: | ||
if case .success(let url) = result { | ||
self.baseURL = url | ||
self.onEmittingNewState?((self.makeRequest(url), nil)) | ||
completion(self.makeRequest(url), nil) | ||
} | ||
} | ||
} | ||
|
@@ -163,14 +122,8 @@ extension FxAWebViewModel { | |
|
||
// MARK: - Commands | ||
extension FxAWebViewModel { | ||
|
||
func dispatchLongPressCommand() { | ||
let hideLongpress = "document.body.style.webkitTouchCallout='none';" | ||
onWantingToExecuteJSScript?(hideLongpress) | ||
} | ||
|
||
func parseAndExecuteSuitableRemoteCommand(basedOn message: WKScriptMessage) { | ||
guard let url = baseURL else { return } | ||
func handle(scriptMessage message: WKScriptMessage) { | ||
guard let url = baseURL, let webView = message.webView else { return } | ||
|
||
let origin = message.frameInfo.securityOrigin | ||
guard origin.`protocol` == url.scheme && origin.host == url.host && origin.port == (url.port ?? 0) else { | ||
|
@@ -183,40 +136,40 @@ extension FxAWebViewModel { | |
let msg = detail["message"] as? [String: Any], let cmd = msg["command"] as? String else { | ||
return | ||
} | ||
|
||
let id = Int(msg["messageId"] as? String ?? "") | ||
handleRemote(command: cmd, id: id, data: msg["data"]) | ||
handleRemote(command: cmd, id: id, data: msg["data"], webView: webView) | ||
} | ||
|
||
// Handle a message coming from the content server. | ||
private func handleRemote(command rawValue: String, id: Int?, data: Any?) { | ||
private func handleRemote(command rawValue: String, id: Int?, data: Any?, webView: WKWebView) { | ||
if let command = RemoteCommand(rawValue: rawValue) { | ||
switch command { | ||
case .login: | ||
if let data = data { | ||
onLogin(data: data) | ||
onLogin(data: data, webView: webView) | ||
} | ||
case .changePassword: | ||
if let data = data { | ||
onPasswordChange(data: data) | ||
onPasswordChange(data: data, webView: webView) | ||
} | ||
case .status: | ||
if let id = id { | ||
onSessionStatus(id: id) | ||
onSessionStatus(id: id, webView: webView) | ||
} | ||
case .deleteAccount, .signOut: | ||
profile.removeAccount() | ||
onDismissController?() | ||
case .profileChanged: | ||
firefoxAccounts.accountManager.peek()?.refreshProfile(ignoreCache: true) | ||
profile.rustFxA.accountManager.peek()?.refreshProfile(ignoreCache: true) | ||
// dismiss keyboard after changing profile in order to see notification view | ||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) | ||
} | ||
} | ||
} | ||
|
||
/// Send a message to web content using the required message structure. | ||
private func runJS(typeId: String, messageId: Int, command: String, data: String = "{}") { | ||
private func runJS(webView: WKWebView, typeId: String, messageId: Int, command: String, data: String = "{}") { | ||
let msg = """ | ||
var msg = { | ||
id: "\(typeId)", | ||
|
@@ -229,12 +182,12 @@ extension FxAWebViewModel { | |
window.dispatchEvent(new CustomEvent('WebChannelMessageToContent', { detail: JSON.stringify(msg) })); | ||
""" | ||
|
||
onWantingToExecuteJSScript?(msg) | ||
webView.evaluateJavaScript(msg) | ||
} | ||
|
||
/// Respond to the webpage session status notification by either passing signed in user info (for settings), or by passing CWTS setup info (in case the user is signing up for an account). This latter case is also used for the sign-in state. | ||
private func onSessionStatus(id: Int) { | ||
guard let fxa = firefoxAccounts.accountManager.peek() else { return } | ||
private func onSessionStatus(id: Int, webView: WKWebView) { | ||
guard let fxa = profile.rustFxA.accountManager.peek() else { return } | ||
let cmd = "fxaccounts:fxa_status" | ||
let typeId = "account_updates" | ||
let data: String | ||
|
@@ -254,7 +207,7 @@ extension FxAWebViewModel { | |
verified: true, | ||
} | ||
} | ||
""" | ||
""" | ||
case .emailLoginFlow, .qrCode: | ||
data = """ | ||
{ capabilities: | ||
|
@@ -263,10 +216,10 @@ extension FxAWebViewModel { | |
""" | ||
} | ||
|
||
runJS(typeId: typeId, messageId: id, command: cmd, data: data) | ||
runJS(webView: webView, typeId: typeId, messageId: id, command: cmd, data: data) | ||
} | ||
|
||
private func onLogin(data: Any) { | ||
private func onLogin(data: Any, webView: WKWebView) { | ||
guard let data = data as? [String: Any], let code = data["code"] as? String, let state = data["state"] as? String else { | ||
return | ||
} | ||
|
@@ -278,14 +231,14 @@ extension FxAWebViewModel { | |
|
||
// Use presence of key `offeredSyncEngines` to determine if this was a new sign-up. | ||
if let engines = data["offeredSyncEngines"] as? [String], engines.count > 0 { | ||
leanPlumClient.track(event: .signsUpFxa) | ||
LeanPlumClient.shared.track(event: .signsUpFxa) | ||
} else { | ||
leanPlumClient.track(event: .signsInFxa) | ||
LeanPlumClient.shared.track(event: .signsInFxa) | ||
} | ||
leanPlumClient.set(attributes: [LPAttributeKey.signedInSync: true]) | ||
LeanPlumClient.shared.set(attributes: [LPAttributeKey.signedInSync: true]) | ||
|
||
let auth = FxaAuthData(code: code, state: state, actionQueryParam: "signin") | ||
firefoxAccounts.accountManager.peek()?.finishAuthentication(authData: auth) { _ in | ||
profile.rustFxA.accountManager.peek()?.finishAuthentication(authData: auth) { _ in | ||
self.profile.syncManager.onAddedAccount() | ||
|
||
// ask for push notification | ||
|
@@ -304,12 +257,12 @@ extension FxAWebViewModel { | |
onDismissController?() | ||
} | ||
|
||
private func onPasswordChange(data: Any) { | ||
private func onPasswordChange(data: Any, webView: WKWebView) { | ||
guard let data = data as? [String: Any], let sessionToken = data["sessionToken"] as? String else { | ||
return | ||
} | ||
|
||
firefoxAccounts.accountManager.peek()?.handlePasswordChanged(newSessionToken: sessionToken) { | ||
profile.rustFxA.accountManager.peek()?.handlePasswordChanged(newSessionToken: sessionToken) { | ||
NotificationCenter.default.post(name: .RegisterForPushNotifications, object: nil) | ||
} | ||
} | ||
|
@@ -320,11 +273,10 @@ extension FxAWebViewModel { | |
let redirectUrl = RustFirefoxAccounts.redirectURL | ||
if let navigationURL = navigationURL { | ||
let expectedRedirectURL = URL(string: redirectUrl)! | ||
if navigationURL.isEquivilent(to: expectedRedirectURL) { | ||
if navigationURL.scheme == expectedRedirectURL.scheme && navigationURL.host == expectedRedirectURL.host && navigationURL.path == expectedRedirectURL.path { | ||
return .cancel | ||
} | ||
} | ||
return .allow | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Notes:
profile
has fxa as a property, and leanplum is used as a shared by-convention and isn't passed around.