Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 7d60ee3

Browse files
committed
[macOS] Set textfield autofill type
macOS supports three autofill types: username, password, and oneTimeCode. Apply these content types when the equivalent Flutter autofill hints are set. Note that enabling autocomplete of username and password in applications will still require setting the "Autofill Credential Provider" and "Associated Domains" entitlements, and for the FlutterTextInputPlugin to be updated to use an NSSecureTextInput input type. Issue: flutter/flutter#120252
1 parent 60c1995 commit 7d60ee3

File tree

2 files changed

+120
-5
lines changed

2 files changed

+120
-5
lines changed

shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,34 @@ typedef NS_ENUM(NSUInteger, FlutterTextAffinity) {
9191
}
9292

9393
// Returns the autofill hint content type, if specified; otherwise nil.
94-
static NSString* GetAutofillContentType(NSDictionary* autofill) {
94+
static NSString* GetAutofillHint(NSDictionary* autofill) {
9595
NSArray<NSString*>* hints = autofill[kAutofillHints];
9696
return hints.count > 0 ? hints[0] : nil;
9797
}
9898

99+
static NSTextContentType GetTextContentType(NSDictionary* configuration)
100+
API_AVAILABLE(macos(11.0)) {
101+
// Check autofill hints.
102+
NSDictionary* autofill = configuration[kAutofillProperties];
103+
if (autofill) {
104+
NSString* hint = GetAutofillHint(autofill);
105+
if ([hint isEqualToString:@"username"]) {
106+
return NSTextContentTypeUsername;
107+
}
108+
if ([hint isEqualToString:@"password"]) {
109+
return NSTextContentTypePassword;
110+
}
111+
if ([hint isEqualToString:@"oneTimeCode"]) {
112+
return NSTextContentTypeOneTimeCode;
113+
}
114+
}
115+
// If no autofill hints, guess based on other attributes.
116+
if ([configuration[kSecureTextEntry] boolValue]) {
117+
return NSTextContentTypePassword;
118+
}
119+
return nil;
120+
}
121+
99122
// Returns YES if configuration describes a field for which autocomplete should be enabled for
100123
// the specified TextInputConfiguration. Autocomplete is enabled by default, but will be disabled
101124
// if the field is password-related, or if the configuration contains no autofill settings.
@@ -113,8 +136,8 @@ static BOOL EnableAutocompleteForTextInputConfiguration(NSDictionary* configurat
113136

114137
// Disable if autofill properties indicate a username/password.
115138
// See: https://github.com/flutter/flutter/issues/119824
116-
NSString* contentType = GetAutofillContentType(autofill);
117-
if ([contentType isEqualToString:@"password"] || [contentType isEqualToString:@"username"]) {
139+
NSString* hint = GetAutofillHint(autofill);
140+
if ([hint isEqualToString:@"password"] || [hint isEqualToString:@"username"]) {
118141
return NO;
119142
}
120143
return YES;
@@ -386,7 +409,9 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
386409
_inputType = inputTypeInfo[kTextInputTypeName];
387410
self.textAffinity = kFlutterTextAffinityUpstream;
388411
self.automaticTextCompletionEnabled = EnableAutocomplete(config);
389-
// TODO(cbracken): support text content types https://github.com/flutter/flutter/issues/120252
412+
if (@available(macOS 11.0, *)) {
413+
self.contentType = GetTextContentType(config);
414+
}
390415

391416
_activeModel = std::make_unique<flutter::TextInputModel>();
392417
}

shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,12 @@ - (bool)testAutocompleteEnabledWhenAutofillSet {
449449

450450
// Verify autocomplete is enabled.
451451
EXPECT_TRUE([plugin isAutomaticTextCompletionEnabled]);
452+
453+
// Verify content type is nil for unsupported content types.
454+
if (@available(macOS 11.0, *)) {
455+
EXPECT_EQ([plugin contentType], nil);
456+
}
457+
452458
return true;
453459
}
454460

@@ -510,7 +516,6 @@ - (bool)testAutocompleteDisabledWhenObscureTextSet {
510516
@"obscureText" : @YES,
511517
@"autofill" : @{
512518
@"uniqueIdentifier" : @"field1",
513-
@"hints" : @[ @"name" ],
514519
@"editingValue" : @{@"text" : @""},
515520
}
516521
}
@@ -555,6 +560,11 @@ - (bool)testAutocompleteDisabledWhenPasswordAutofillSet {
555560

556561
// Verify autocomplete is disabled.
557562
EXPECT_FALSE([plugin isAutomaticTextCompletionEnabled]);
563+
564+
// Verify content type is password.
565+
if (@available(macOS 11.0, *)) {
566+
EXPECT_EQ([plugin contentType], NSTextContentTypePassword);
567+
}
558568
return true;
559569
}
560570

@@ -608,6 +618,86 @@ - (bool)testAutocompleteDisabledWhenAutofillGroupIncludesPassword {
608618
return true;
609619
}
610620

621+
- (bool)testContentTypeWhenAutofillTypeIsUsername {
622+
// Set up FlutterTextInputPlugin.
623+
id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
624+
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
625+
OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
626+
[engineMock binaryMessenger])
627+
.andReturn(binaryMessengerMock);
628+
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
629+
nibName:@""
630+
bundle:nil];
631+
FlutterTextInputPlugin* plugin =
632+
[[FlutterTextInputPlugin alloc] initWithViewController:viewController];
633+
634+
// Set input client 1.
635+
[plugin handleMethodCall:[FlutterMethodCall
636+
methodCallWithMethodName:@"TextInput.setClient"
637+
arguments:@[
638+
@(1), @{
639+
@"inputAction" : @"action",
640+
@"inputType" : @{@"name" : @"inputName"},
641+
@"autofill" : @{
642+
@"uniqueIdentifier" : @"field1",
643+
@"hints" : @[ @"username" ],
644+
@"editingValue" : @{@"text" : @""},
645+
}
646+
}
647+
]]
648+
result:^(id){
649+
}];
650+
651+
// Verify autocomplete is disabled.
652+
EXPECT_FALSE([plugin isAutomaticTextCompletionEnabled]);
653+
654+
// Verify content type is username.
655+
if (@available(macOS 11.0, *)) {
656+
EXPECT_EQ([plugin contentType], NSTextContentTypeUsername);
657+
}
658+
return true;
659+
}
660+
661+
- (bool)testContentTypeWhenAutofillTypeIsOneTimeCode {
662+
// Set up FlutterTextInputPlugin.
663+
id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
664+
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
665+
OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
666+
[engineMock binaryMessenger])
667+
.andReturn(binaryMessengerMock);
668+
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
669+
nibName:@""
670+
bundle:nil];
671+
FlutterTextInputPlugin* plugin =
672+
[[FlutterTextInputPlugin alloc] initWithViewController:viewController];
673+
674+
// Set input client 1.
675+
[plugin handleMethodCall:[FlutterMethodCall
676+
methodCallWithMethodName:@"TextInput.setClient"
677+
arguments:@[
678+
@(1), @{
679+
@"inputAction" : @"action",
680+
@"inputType" : @{@"name" : @"inputName"},
681+
@"autofill" : @{
682+
@"uniqueIdentifier" : @"field1",
683+
@"hints" : @[ @"oneTimeCode" ],
684+
@"editingValue" : @{@"text" : @""},
685+
}
686+
}
687+
]]
688+
result:^(id){
689+
}];
690+
691+
// Verify autocomplete is disabled.
692+
EXPECT_FALSE([plugin isAutomaticTextCompletionEnabled]);
693+
694+
// Verify content type is username.
695+
if (@available(macOS 11.0, *)) {
696+
EXPECT_EQ([plugin contentType], NSTextContentTypeOneTimeCode);
697+
}
698+
return true;
699+
}
700+
611701
- (bool)testFirstRectForCharacterRange {
612702
id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
613703
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));

0 commit comments

Comments
 (0)