-
Notifications
You must be signed in to change notification settings - Fork 6k
Add autofill save for iOS and Android #18643
Add autofill save for iOS and Android #18643
Conversation
| // that adopts the UITextInput protocol, automatic strong password seems to | ||
| // currently only support UITextFields, and password saving only supports | ||
| // UITextFields and UITextViews, as of iOS 13.5. | ||
| @interface FlutterSecureTextInputView : FlutterTextInputView |
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.
Changing FlutterTextInputView to a UITextField seems to require a lot of code changes so I chose this hack instead. From the documentation I think not supporting custom UITextInput is a bug in UIKit (unfortunately it happens on both iOS 12 & iOS 13), once it's fixed we can get rid of this class.
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.
Unrecognized selectors need to be forwarded to the shared text field instance because UIKit's keyboard implementation calls private UITextField methods (we tricked it into thinking this is a UITextField instance).
justinmc
left a comment
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 from my point of view, but definitely wait for the other reviewers to take a look as well. The FlutterSecureTextInputView hack seems reasonable to me for now.
7cac527 to
c2960bb
Compare
c2960bb to
6499cf5
Compare
|
I'll test this locally with web engine changes and let you know. It might take a day, sorry for the inconvenience. |
c97555c to
5d09ab5
Compare
5d09ab5 to
fd2560f
Compare
gaaclarke
left a comment
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.
Reviewed iOS portion. It's an intense PR but great work. I just had one legitimate concern regarding the static input field.
| } | ||
|
|
||
| static NSString* uniqueIdFromDictionary(NSDictionary* dictionary) { | ||
| static NSString* autofillIdFromDictionary(NSDictionary* dictionary) { |
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 sentence docstring would be nice.
| + (UITextField*)textField { | ||
| static UITextField* _textField = nil; | ||
| if (_textField == nil) { | ||
| _textField = [[UITextField alloc] 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.
Why not have a UITextField per FlutterSecureTextInputView? This seems a bit risky considering we are forwarding unknown messages all to a singular instance.
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.
The unrecognized selectors sent to FlutterSecureTextInputView that I'm seeing in my demo app:
rightView
leftView
textAlignment
font
_setContentCoverViewMode:
_setContentCoverView:
_setBackgroundCoverViewMode:
_setBackgroundCoverView:
_layoutManager
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 think the assumption here is that the UITextField instance is just a stateless (for the states that matter to us) selector sponge. For the selectors I'm seeing the hack seems to work fine. If this hack doesn't work then we should probably switch to a completely different approach (e.g., subclassing UITextField).
If there are other selectors that may set the UITextField to a broken state, then having separate instances probably won't prevent that from happening? If we make each FlutterSecureTextInputView use its own UITextField, we are going to reuse the UITextField anyway when we reuse the FlutterSecureTextInputView.
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.
It isn't stateless though since we are forwarding messages to it that could be setting all kinds of state.
[inputView1 setAccessibilityHint:@"hey"];
print(inputView2.accessibilityHint);for example
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.
Updated to create a UITextField per instance. What I meant was if any of the states those unrecognized selectors can change matter to us or iOS itself, then using this hack is pretty much a lost cause.
| if (!autofillIdFromDictionary(configuration)) { | ||
| return NO; | ||
| } | ||
| NSDictionary* autofill = configuration[@"autofill"]; |
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 all these string literals that are shared across the code it would be nice to have const variables for them to avoid errors.
static NSString *const APIPathFoo = APIBasePath @"foo";
| } | ||
|
|
||
| - (void)setIsVisibleToAutofill:(BOOL)isVisibleToAutofill { | ||
| self.frame = isVisibleToAutofill ? CGRectMake(0, 0, 1, 1) : CGRectZero; |
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 might merit a comment since it seems a bit hacky to change the frame based on something like if it is visible to autofill.
| @property(nonatomic, retain) FlutterTextInputView* nonAutofillInputView; | ||
| @property(nonatomic, retain) FlutterTextInputView* nonAutofillSecureInputView; | ||
| @property(nonatomic, retain) NSMutableArray<FlutterTextInputView*>* inputViews; | ||
| @property(nonatomic, retain) FlutterTextInputView* reusableInputView; |
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: @property(nonatomic, strong)
| NSDictionary* autofill = configuration[@"autofill"]; | ||
| inputView.textInputDelegate = _textInputDelegate; | ||
| [inputView configureWithDictionary:field]; | ||
| return inputView; |
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.
super nit: move the autorelease to the return statement to make auditing a bit easier
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.
the inputView is not necessarily a new object, putting autorelease at the end would make it a zombie when it's reused.
| static const char _kTextAffinityUpstream[] = "TextAffinity.upstream"; | ||
|
|
||
| #pragma mark - TextInputConfiguration Field Names | ||
| static NSString* const _kSecureTextEntry = @"obscureText"; |
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.
You shouldn't have the _ prefix here: https://google.github.io/styleguide/objcguide.html#constants
|
There's a crash in |
|
The crash should be fixed now. |
gaaclarke
left a comment
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
| @end | ||
|
|
||
| @implementation FlutterSecureTextInputView { | ||
| UITextField* _textField; |
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: This isn't necessary since the property will declare 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 think this is required because the property is readOnly and I overrode its getter so it will not get synthesized?
part of flutter/flutter#55613
framework PR: flutter/flutter#58731
Android
Android saves the autofillable input fields by value under the hood. We just have to call
AutofillManager#commitorAutofillManager#commitat the right time.iOS
iOS does some accounting to keep track of possible password autofill fields (but it does not save the input by value, instead it keeps track of the references to the input views).
When an input view is being removed from the view hierarchy, iOS uses heuristics to determine if the view is password related and will try to extract & save the username/password.
For iOS, I'm keeping the password-related input fields in the view hierarchy until we're told to start a new autofill context (
TextInput.AutofillContext.{commit, cancel}).iOS demo
The username & password fields (under the "login" section) initially didn't have any autofill candidates. After using automatic strong password to generate a new password (the "sign up" section) and tapping submit to save the new username/password pairs, a new autofill entry is added to the quick type bar when revisiting the username/password fields.