Skip to content

Commit

Permalink
Merge pull request #1181 from Automattic/lantean/magic-links-auth-mk4
Browse files Browse the repository at this point in the history
Magic Link Login: Mk 4
  • Loading branch information
jleandroperez authored Jul 8, 2024
2 parents d18a062 + 18c255e commit 698da14
Show file tree
Hide file tree
Showing 10 changed files with 350 additions and 6 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
2.21
-----
- Added shortcuts to Simplenote Mac!!
- New Magic Link Authentication support

2.20
-----
Expand Down
8 changes: 8 additions & 0 deletions Simplenote.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@
B5CD5F7F241AC69900D0260F /* NSProcessInfo+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CD5F7D241AC69900D0260F /* NSProcessInfo+Simplenote.swift */; };
B5D21CB824881EF600D57A34 /* Array+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D21CB624881EF600D57A34 /* Array+Simplenote.swift */; };
B5D3FCCD201F906E00A813B7 /* StatusChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = B5D3FCCA201F906E00A813B7 /* StatusChecker.m */; };
B5D586CE2C23793900F3ADCC /* MagicLinkConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D586CD2C23793900F3ADCC /* MagicLinkConfirmationView.swift */; };
B5D586D02C2396A900F3ADCC /* MagicLinkRequestedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D586CF2C2396A900F3ADCC /* MagicLinkRequestedView.swift */; };
B5DD0F922476309000C8DD41 /* NoteTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DD0F902476309000C8DD41 /* NoteTableCellView.swift */; };
B5E061782450AEDA0076111A /* ToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E061762450AEDA0076111A /* ToolbarView.swift */; };
B5E086292448B57200DEF476 /* HeaderTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E086272448B57200DEF476 /* HeaderTableCellView.swift */; };
Expand Down Expand Up @@ -682,6 +684,8 @@
B5D21CB624881EF600D57A34 /* Array+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Simplenote.swift"; sourceTree = "<group>"; };
B5D3FCCA201F906E00A813B7 /* StatusChecker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StatusChecker.m; sourceTree = "<group>"; };
B5D3FCCB201F906E00A813B7 /* StatusChecker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StatusChecker.h; sourceTree = "<group>"; };
B5D586CD2C23793900F3ADCC /* MagicLinkConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagicLinkConfirmationView.swift; sourceTree = "<group>"; };
B5D586CF2C2396A900F3ADCC /* MagicLinkRequestedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagicLinkRequestedView.swift; sourceTree = "<group>"; };
B5DD0F902476309000C8DD41 /* NoteTableCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteTableCellView.swift; sourceTree = "<group>"; };
B5E061762450AEDA0076111A /* ToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarView.swift; sourceTree = "<group>"; };
B5E073B424BF8BD900139912 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1305,6 +1309,8 @@
B5177CBA25EEB01600A8D834 /* AuthViewController+Swift.swift */,
B597429225E97ADC0063DDD2 /* AuthViewController.xib */,
B545DDD12C2313BA00A8FD89 /* AuthenticationMode.swift */,
B5D586CF2C2396A900F3ADCC /* MagicLinkRequestedView.swift */,
B5D586CD2C23793900F3ADCC /* MagicLinkConfirmationView.swift */,
B5177CC325EEBF6900A8D834 /* SignupVerificationViewController.swift */,
B5177CC925EEBF8200A8D834 /* SignupVerificationViewController.xib */,
);
Expand Down Expand Up @@ -2066,6 +2072,7 @@
B58117C525B9D57F00927E0C /* AccountVerificationViewController.swift in Sources */,
B5AF76C824A3F00600B7D530 /* TagListState.swift in Sources */,
B5469FD12587FCD9007ED7BE /* NSSortDescriptor+Simplenote.swift in Sources */,
B5D586D02C2396A900F3ADCC /* MagicLinkRequestedView.swift in Sources */,
466FFEA817CC10A800399652 /* main.m in Sources */,
B518D37C2507BFA4006EA7F8 /* Note+Interlinks.swift in Sources */,
B58117E925B9EE7B00927E0C /* Button.swift in Sources */,
Expand Down Expand Up @@ -2262,6 +2269,7 @@
B528D684257FF15000C1EEA4 /* MainWindowController.swift in Sources */,
37AE49C31FFEB92A00FCB165 /* SPMarkdownParser.m in Sources */,
B5CBB05D2419714B0003C271 /* NSAttributedStringToMarkdownConverter.swift in Sources */,
B5D586CE2C23793900F3ADCC /* MagicLinkConfirmationView.swift in Sources */,
B5E96B671BDE732500D707F5 /* AuthViewController.m in Sources */,
B5EB3AD52458C48D0089858D /* SimplenoteAppDelegate+Swift.swift in Sources */,
B5EB3AD22458B9940089858D /* NSNotification+Simplenote.m in Sources */,
Expand Down
42 changes: 42 additions & 0 deletions Simplenote/AuthViewController+Swift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ extension AuthViewController {
var passwordText: String {
passwordField.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
}

var authWindowController: AuthWindowController? {
view.window?.windowController as? AuthWindowController
}
}

// MARK: - Refreshing
Expand Down Expand Up @@ -195,6 +199,36 @@ extension AuthViewController {
}
}

@objc
func performLoginWithEmailRequest() {
Task {
await performLoginWithEmailRequestInTask()
}
}

@MainActor
func performLoginWithEmailRequestInTask() async {
defer {
stopActionAnimation()
setInterfaceEnabled(true)
}

startActionAnimation()
setInterfaceEnabled(false)

do {
let email = usernameText
let remote = LoginRemote()
try await remote.requestLoginEmail(email: email)

presentMagicLinkRequestedView(email: email)

} catch {
let statusCode = (error as? RemoteError)?.statusCode ?? .zero
self.showAuthenticationError(forCode: statusCode, responseString: nil)
}
}

@IBAction
func handleNewlineInField(_ field: NSControl) {
if field.isEqual(passwordField.textField) {
Expand Down Expand Up @@ -235,6 +269,14 @@ extension AuthViewController {
let vc = SignupVerificationViewController(email: email, authenticator: authenticator)
view.window?.transition(to: vc)
}

func presentMagicLinkRequestedView(email: String) {
guard let authWindowController else {
return
}

authWindowController.switchToMagicLinkRequestedUI(email: email)
}
}

// MARK: - Login Error Handling
Expand Down
15 changes: 14 additions & 1 deletion Simplenote/AuthViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,15 @@ - (void)pressedLogInWithPassword {
}

- (void)pressedLoginWithMagicLink {
NSLog(@"# TODO: Request Magic Link!!");
[SPTracker trackUserRequestedLoginLink];

[self clearAuthenticationError];

if (![self validateSignInWithMagicLink]) {
return;
}

[self performLoginWithEmailRequest];
}

- (void)pressedSignUp {
Expand Down Expand Up @@ -281,6 +289,11 @@ - (BOOL)validateSignIn {
[self validatePasswordSecurity];
}

- (BOOL)validateSignInWithMagicLink {
return [self validateConnection] &&
[self validateUsername];
}

- (BOOL)validateSignUp {
return [self validateConnection] &&
[self validateUsername];
Expand Down
78 changes: 78 additions & 0 deletions Simplenote/AuthWindowController.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Foundation
import AppKit
import SwiftUI


// MARK: - AuthWindowController
//
Expand All @@ -18,6 +20,10 @@ class AuthWindowController: NSWindowController, SPAuthenticationInterface {
}

// MARK: - Initializer

deinit {
stopListeningToNotifications()
}

init() {
let window = NSWindow(contentViewController: authViewController)
Expand All @@ -29,9 +35,81 @@ class AuthWindowController: NSWindowController, SPAuthenticationInterface {
window.backgroundColor = .white

super.init(window: window)
startListeningToNotifications()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}


// MARK: - Notifications
//
extension AuthWindowController {

func startListeningToNotifications() {
let nc = NotificationCenter.default
nc.addObserver(self, selector: #selector(displayAuthenticationInProgress), name: .magicLinkAuthWillStart, object: nil)
}

func stopListeningToNotifications() {
NotificationCenter.default.removeObserver(self)
}

@objc
func displayAuthenticationInProgress(_ sender: Notification) {
DispatchQueue.main.async {
self.switchToMagicLinkConfirmationUI()
}
}
}


// MARK: - User Interface
//
extension AuthWindowController {

func switchToAuthenticationUI() {
guard let window else {
return
}

let authViewController = AuthViewController()
authViewController.authenticator = authenticator
window.transition(to: authViewController)
}

func switchToMagicLinkRequestedUI(email: String) {
guard let window else {
return
}

/// Renders a UI that indicates a Magic Link has been requested
///
var rootView = MagicLinkRequestedView(email: email)
rootView.onDismissRequest = { [weak self] in
self?.switchToAuthenticationUI()
}

let hostingController = NSHostingController(rootView: rootView)
window.switchContentViewController(to: hostingController)
}

func switchToMagicLinkConfirmationUI() {
guard let window else {
return
}

/// Renders a spinner while we attempt to authorize a Magic Link.
/// It'll pick up the `magicLinkAuthDidFail` Notification, and will display an error, if needed.
///
var rootView = MagicLinkConfirmationView()
rootView.onDismissRequest = { [weak self] in
self?.switchToAuthenticationUI()
}

let hostingController = NSHostingController(rootView: rootView)
window.switchContentViewController(to: hostingController)
}
}
4 changes: 2 additions & 2 deletions Simplenote/MagicLinkAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ private extension MagicLinkAuthenticator {
Task { @MainActor in
do {
let confirmation = try await loginRemote.requestLoginConfirmation(authKey: authKey, authCode: authCode)

NSLog("[MagicLinkAuthenticator] Should auth with token \(confirmation.syncToken)")
authenticator.authenticate(withUsername: confirmation.username, token: confirmation.syncToken)

NotificationCenter.default.post(name: .magicLinkAuthDidSucceed, object: nil)
SPTracker.trackUserConfirmedLoginLink()

Expand Down
78 changes: 78 additions & 0 deletions Simplenote/MagicLinkConfirmationView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import Foundation
import SwiftUI


// MARK: - MagicLinkConfirmationView
//
struct MagicLinkConfirmationView: View {

@State var displaysInvalidLink = false

var onDismissRequest: (() -> Void)?

var body: some View {
VStack(spacing: Metrics.stackSpacing) {
if displaysInvalidLink {
invalidLinkText
invalidLinkButton
} else {
authorizingText
progressIndicator
}
}
.padding()
.background(.white)
.frame(width: Metrics.expectedSize.width, height: Metrics.expectedSize.height)
.onReceive(NotificationCenter.default.publisher(for: .magicLinkAuthDidFail)) { _ in
Task { @MainActor in
displaysInvalidLink = true
}
}
}

private var authorizingText: some View {
Text("Logging In...")
.font(.title3)
.foregroundColor(.gray)
}

private var progressIndicator: some View {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.controlSize(.small)
}

private var invalidLinkText: some View {
Text("Link was no longer valid")
.font(.title3)
.foregroundColor(.gray)
}

private var invalidLinkButton: some View {
SwiftUI.Button(action: switchToAuthenticationUI) {
Text("Accept")
.fontWeight(.bold)
.foregroundStyle(Color(nsColor: .simplenoteBrandColor))
.onHover { inside in
if inside {
NSCursor.pointingHand.set()
} else {
NSCursor.arrow.set()
}
}
}
.buttonStyle(PlainButtonStyle())
}

func switchToAuthenticationUI() {
onDismissRequest?()
}
}


// MARK: - Metrics
//
private struct Metrics {
static let stackSpacing: CGFloat = 20
static let expectedSize = CGSize(width: 380, height: 200)
}
Loading

0 comments on commit 698da14

Please sign in to comment.