Skip to content
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

Requesting review for SwiftUI Passwordless demo app #1015

Merged
merged 14 commits into from
Jul 31, 2020

Conversation

ncooke3
Copy link
Member

@ncooke3 ncooke3 commented Jul 17, 2020

Not sure where this project will live yet (will talk with dev rel) but for the sake of review, I've created a swiftui directory in this repo for this review (and to potentially house future SwiftUI projects?).

Please Review:
PasswordlessSwiftUIApp.swift
ContentView.swift

TODO:

  • README

Copy link
Member Author

@ncooke3 ncooke3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving a few comments.

Comment on lines 41 to 48
.onOpenURL { (url) in
let link = url.absoluteString
if Auth.auth().isSignIn(withEmailLink: link) {
passwordlessSignIn(email: email, link: link) { (success) in
self.presentSheet = success
}
}
}
Copy link
Member Author

@ncooke3 ncooke3 Jul 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not call any Dynamic Links API's. Interestingly the above code works well regardless. If we use the dynamic link APIs to verify the incoming link is indeed a dynamic link, the approach below works:

   // The above approach does not use the dynamic link APIs.
   .onOpenURL { (url) in
     DynamicLinks.dynamicLinks().handleUniversalLink(url) { (dynamicLink, error) in
       guard error == nil, let link = dynamicLink?.url?.absoluteString else {
         print(error!)
         return
       }

       if Auth.auth().isSignIn(withEmailLink: link) {
         passwordlessSignIn(email: email, link: link) { (success) in
           self.presentSheet = success
         }
       }
     }
   }

After chatting with DL team, it seems like calling handleUniversalLink() does handles reopening attribution and some analytics for the link. I'm unsure whether it is still necessary here though (because it works well without it so I'm not sure about issues arising when it is left out). Thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also unsure on this bit, but we should chat about it further to understand the implications. Also to better understand: could we split those two calls up? Not saying it's the right idea, just curious.

.openURL { url in 
  // Handle universal link
  DynamicLinks.dynamicLinks()....
}
.openURL { url in 
  guard Auth.auth().isSignin(withEmailLink: url) else { return }

  // Handle auth sign in
}

Comment on lines 165 to 181
// MARK: Example Auth View Modifier

extension View {
func handlePasswordlessLogin(forEmail email: String,
completion: @escaping (Bool)->()) -> some View {
self.onOpenURL { (url) in
let link = url.absoluteString
if Auth.auth().isSignIn(withEmailLink: link) {
passwordlessSignIn(email: email, link: link) { (success) in
completion(success)
}
} else {
completion(false)
}
}
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps Firebase could start including view modifiers like this one, for example.

    /// It's also possible to refactor the code on line 41 by using a view modifier like:
    .handlePasswordlessLogin(forEmail: email) { (success) in
      self.presentSheet = success
    }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm definitely intrigued by this! I think it would make sense to do since it provides a lot more context than openURL and the (arguably) boiler plate code to handle the sign in.

Copy link
Member

@paulb777 paulb777 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally LGTM

Leaving approval to @ryanwilson and directory structure question to @morganchen12

@@ -0,0 +1,21 @@
# Uncomment the next line to define a global platform for your project
platform :ios, '14.0'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is 13.0 possible?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

14 is required for the new @main and entire-SwiftUI lifecycle (instead of AppDelegate)

Copy link
Member

@ryanwilson ryanwilson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking pretty good so far! Added some comments, also it would be helpful to have screenshots uploaded as well for each revision (that changes the UI at all) to better match between the two.

@@ -0,0 +1,181 @@
//
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: can you replace this and the other headers with the full copyright?

Comment on lines 24 to 26
Spacer()
.frame(height: 0.0)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point here in having a spacer with a specified 0 height in a VStack?

NavigationView {
VStack(spacing: 50.0) {
Text("Authenticate users with only their email, no password required!")
.frame(maxWidth: .infinity, alignment: .leading)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary? Or what are you trying to accomplish here? If it was truncating the width, you can likely get away with setting the maximum number of lines to 0.

/// Main view where user can login in using Email Link Authentication.
struct ContentView: View {
@State private var email: String = ""
@State private var presentSheet = false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is stateful, having it isPresentingSheet probably makes more sense.

}
.padding()
.navigationBarTitle("Passwordless Login")

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: empty space.

Comment on lines 41 to 48
.onOpenURL { (url) in
let link = url.absoluteString
if Auth.auth().isSignIn(withEmailLink: link) {
passwordlessSignIn(email: email, link: link) { (success) in
self.presentSheet = success
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also unsure on this bit, but we should chat about it further to understand the implications. Also to better understand: could we split those two calls up? Not saying it's the right idea, just curious.

.openURL { url in 
  // Handle universal link
  DynamicLinks.dynamicLinks()....
}
.openURL { url in 
  guard Auth.auth().isSignin(withEmailLink: url) else { return }

  // Handle auth sign in
}

Comment on lines 52 to 54

}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: two extra blank lines that can be removed.

struct CustomStyledTextField: View {
@Binding var text: String
let placeholder: String
let symbol: String
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: symbolName, to make sure someone doesn't try to pass in unicode characters as the symbols :P

@main
struct PasswordlessSwiftUIApp: App {

init() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Can you add a comment here saying this is the new place to add FirebaseApp.configure() since there's no more appDelegate?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Firebase features that make use of AppDelegate swizzling, developers will probably need to use @UIApplicationDelegateAdaptor injection. Hence, I'd remove "recommended" from the comment.

Comment on lines 165 to 181
// MARK: Example Auth View Modifier

extension View {
func handlePasswordlessLogin(forEmail email: String,
completion: @escaping (Bool)->()) -> some View {
self.onOpenURL { (url) in
let link = url.absoluteString
if Auth.auth().isSignIn(withEmailLink: link) {
passwordlessSignIn(email: email, link: link) { (success) in
completion(success)
}
} else {
completion(false)
}
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm definitely intrigued by this! I think it would make sense to do since it provides a lot more context than openURL and the (arguably) boiler plate code to handle the sign in.

@morganchen12
Copy link
Contributor

Directory structure LGTM. Once we have full coverage of SwiftUI samples we can move the SwiftUI samples to root and put the old samples in a subdirectory for better discoverability.

Comment on lines 35 to 37
CustomStyledButton(title: "Send Sign In Link") {
sendSignInLink(to: email)
sendSignInLink()
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With no email now, we should be able to just use this function as an argument:

CustomStylesButton(title: "Send Sign In Link", action: sendSigninLink)

Comment on lines 48 to 56
passwordlessSignIn(email: email, link: link) { user, error in
isPresentingSheet = user?.isEmailVerified ?? false
}
}
}
.sheet(isPresented: $presentSheet) {
.sheet(isPresented: $isPresentingSheet) {
SuccessView(email: $email)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thought of a potential race condition here: if someone sends a sign in link, changes the email in the text field, then goes to their email and logs in, we'll show that they're logged in as the content of the text field, not the email address they actually used.

Can you think of some ways to get around this without changing the UI?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update. When this code is triggered on a URL open, the email passed into passwordlessSignIn will be the current email typed in the textfield. passwordlessSignIn really just wraps Auth's "sign in with email and link method", which matches up the link and email and will return an error if the email and email the link was sent to don't match.

So I dont think this race condition will happen because if a user enters email A and sends the link. Then changes the textfield to email B and then goes to open the link sent to email A. They will not be signed in when returning to the app because the link corresponding with email A no longer corresponds to the binded email property, which is now email B.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One could always keep the latest email the link was sent to when a user taps the "send sign in link" button. But then what if they send the link to 5 emails and only open the link on the 1,2,3, or 4th? So I think this might be ok to leave as it with the idea that whatever email is in the textfield should represent the email the users wants to login in with.

}

private func passwordlessSignIn(email: String, link: String,
completion: @escaping (_ user: User?, _ error: Error?) -> ()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should take advantage of the Result type here instead of providing two optionals in the callback.

Comment on lines 110 to 114
Spacer()
Text(title)
.padding()
.accentColor(.white)
Spacer()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work as is? It's unclear how these are assembled - is this an HStack, VStack, ZStack? I'm assuming it's just a Group but it's not clear.

completion(.failure(error))
} else {
print("✔ Authentication was successful.")
completion(.success(Auth.auth().currentUser))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be result?.user instead of getting the singleton again.

This doesn't need changing right now, just wanted to point out this is an example where our API could be improved - right now you're forced to return an optional User object (or you could force unwrap it) when there:should be only 2 use cases: successfully signed in, or an error.

If we wanted to handle this case fully, we could check if result is also nil and wrap our own custom error, returning it in the completion handler. Don't think it's fully necessary but thought it was a good learning opportunity.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes- i understand!

Comment on lines +115 to +121
HStack {
Spacer()
Text(title)
.padding()
.accentColor(.white)
Spacer()
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: can you add a comment about why you're wrapping the text with two spacers in an H stack?

}
static var previews: some View {
ContentView()
}
}

// MARK: Example Auth View Modifier

extension View {
func handlePasswordlessLogin(forEmail email: String,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say if we're going to use this, let's keep it, otherwise let's delete it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good if you want to use it - you may need to change the String argument to Binding<String> though (I think...)

Copy link
Member

@ryanwilson ryanwilson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - thanks for working through the comments! Not sure what we want to do yet with the FDL changes but I'd be fine landing this then adding it after if it's necessary.

@main
struct PasswordlessSwiftUIApp: App {

/// With the removal of `AppDelegate`, it is recommended to configure Firbase here.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: Firbase -> Firebase


private func sendSignInLink() {
let actionCodeSettings = ActionCodeSettings()
actionCodeSettings.url = URL(string: "https://passwordlessswiftui.page.link/demo_login")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it might be a good idea to extract project-dependent constants and move them to a configuration struct that developers can then update with their project-specific values.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as project constants, I can think of the above URL and some of the button titles, textfield placeholders, etc. Are these items I should move to a configuration struct?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking mostly of the URL, which would also benefit from being decomposed into the prefix and short link. Button titles etc. should be dealt with proper i18n tools (which might be beyond the scope of this sample, but I'll leave it to @ryanwilson to comment on how stringent we are with i18n in our code samples).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep I think we can leave the i18n out for this - breaking the project constant out to a separate file SGTM, no other variables are needed there.

@ncooke3 ncooke3 requested a review from ryanwilson July 28, 2020 20:59
@ncooke3
Copy link
Member Author

ncooke3 commented Jul 28, 2020

@ryanwilson Following up on our offline discussion yesterday about error handling... What are your thoughts on this latest commit? I experimented with a few things but thought this may be a nice way to display errors using Alert?

@ryanwilson
Copy link
Member

@ryanwilson Following up on our offline discussion yesterday about error handling... What are your thoughts on this latest commit? I experimented with a few things but thought this may be a nice way to display errors using Alert?

I like it - thanks for doing so!

Copy link
Member

@ryanwilson ryanwilson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few small formatting nits, can you change this from a draft request so the other GitHub checks run? I converted it and forgot it's only Travis running.

new lifeycle [APIs](https://developer.apple.com/documentation/swiftui/app)
that enabled developers to build apps entirely with
[SwiftUI](https://developer.apple.com/xcode/swiftui/). This project uses these
latest iOS14 APIs in conjunction with Firebase to provide a clear
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: space between iOS and 14

@@ -0,0 +1,42 @@
# Using Firebase Auth's Email Link Login with SwiftUI
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: newline after the #

latest iOS14 APIs in conjunction with Firebase to provide a clear
example of how to build modern apps with Firebase and SwiftUI.

# Getting Started
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: newline after the #

Getting this project up and running is similar to the other quickstarts
found in this repo. The following steps will clone the project,
install its dependencies, and open the `.xcworkspace` project in Xcode.
```bash
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: newline before this.

$ open PasswordlessSwiftUI.xcworkspace
```

# Finished Demo
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: newline after the #

@ryanwilson ryanwilson marked this pull request as ready for review July 31, 2020 14:09
@ncooke3 ncooke3 merged commit 78b3962 into master Jul 31, 2020
@ncooke3 ncooke3 deleted the nc-passwordless-swiftui branch December 18, 2023 17:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants