-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Conversation
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.
Leaving a few comments.
.onOpenURL { (url) in | ||
let link = url.absoluteString | ||
if Auth.auth().isSignIn(withEmailLink: link) { | ||
passwordlessSignIn(email: email, link: link) { (success) in | ||
self.presentSheet = success | ||
} | ||
} | ||
} |
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.
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?
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.
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
}
// 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) | ||
} | ||
} | ||
} | ||
} |
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.
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
}
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.
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.
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.
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' |
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.
Is 13.0 possible?
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.
14 is required for the new @main
and entire-SwiftUI lifecycle (instead of AppDelegate)
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.
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 @@ | |||
// |
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.
Nit: can you replace this and the other headers with the full copyright?
Spacer() | ||
.frame(height: 0.0) | ||
|
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.
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) |
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.
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 |
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.
Since this is stateful, having it isPresentingSheet
probably makes more sense.
} | ||
.padding() | ||
.navigationBarTitle("Passwordless Login") | ||
|
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.
Nit: empty space.
.onOpenURL { (url) in | ||
let link = url.absoluteString | ||
if Auth.auth().isSignIn(withEmailLink: link) { | ||
passwordlessSignIn(email: email, link: link) { (success) in | ||
self.presentSheet = success | ||
} | ||
} | ||
} |
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.
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
}
|
||
} | ||
|
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.
Nit: two extra blank lines that can be removed.
struct CustomStyledTextField: View { | ||
@Binding var text: String | ||
let placeholder: String | ||
let symbol: String |
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.
Nit: symbolName
, to make sure someone doesn't try to pass in unicode characters as the symbols :P
@main | ||
struct PasswordlessSwiftUIApp: App { | ||
|
||
init() { |
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.
Nit: Can you add a comment here saying this is the new place to add FirebaseApp.configure()
since there's no more appDelegate?
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.
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.
// 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) | ||
} | ||
} | ||
} | ||
} |
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.
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.
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. |
CustomStyledButton(title: "Send Sign In Link") { | ||
sendSignInLink(to: email) | ||
sendSignInLink() | ||
} |
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.
With no email now, we should be able to just use this function as an argument:
CustomStylesButton(title: "Send Sign In Link", action: sendSigninLink)
passwordlessSignIn(email: email, link: link) { user, error in | ||
isPresentingSheet = user?.isEmailVerified ?? false | ||
} | ||
} | ||
} | ||
.sheet(isPresented: $presentSheet) { | ||
.sheet(isPresented: $isPresentingSheet) { | ||
SuccessView(email: $email) | ||
} | ||
} |
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.
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?
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.
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.
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.
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?) -> ()) { |
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.
We should take advantage of the Result
type here instead of providing two optionals in the callback.
Spacer() | ||
Text(title) | ||
.padding() | ||
.accentColor(.white) | ||
Spacer() |
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.
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)) |
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.
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.
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.
yes- i understand!
HStack { | ||
Spacer() | ||
Text(title) | ||
.padding() | ||
.accentColor(.white) | ||
Spacer() | ||
} |
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.
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, |
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.
I would say if we're going to use this, let's keep it, otherwise let's delete it.
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.
I'm good if you want to use it - you may need to change the String
argument to Binding<String>
though (I think...)
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.
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. |
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.
Typo: Firbase -> Firebase
|
||
private func sendSignInLink() { | ||
let actionCodeSettings = ActionCodeSettings() | ||
actionCodeSettings.url = URL(string: "https://passwordlessswiftui.page.link/demo_login") |
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.
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.
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.
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?
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.
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).
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.
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.
@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 |
I like it - thanks for doing so! |
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.
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 |
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.
Nit: space between iOS and 14
@@ -0,0 +1,42 @@ | |||
# Using Firebase Auth's Email Link Login with SwiftUI |
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.
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 |
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.
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 |
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.
Nit: newline before this.
$ open PasswordlessSwiftUI.xcworkspace | ||
``` | ||
|
||
# Finished Demo |
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.
Nit: newline after the #
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 futureSwiftUI
projects?).Please Review:
•
PasswordlessSwiftUIApp.swift
•
ContentView.swift
TODO: