Merge pull request #3160 from stuartbreckenridge/main

[iOS] Add/Update Account Can Use 1Password
This commit is contained in:
Maurice Parker 2021-06-16 20:07:18 -05:00 committed by GitHub
commit 72eacb73b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1120 additions and 78 deletions

View File

@ -132,6 +132,7 @@
17D5F17124B0BC6700375168 /* SidebarToolbarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D5F17024B0BC6700375168 /* SidebarToolbarModel.swift */; };
17D5F17224B0BC6700375168 /* SidebarToolbarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D5F17024B0BC6700375168 /* SidebarToolbarModel.swift */; };
17D5F19524B0C1DD00375168 /* SidebarToolbarModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 172199F024AB716900A31D04 /* SidebarToolbarModifier.swift */; };
17D7586F2679C21800B17787 /* OnePasswordExtension.m in Sources */ = {isa = PBXBuildFile; fileRef = 17D7586E2679C21800B17787 /* OnePasswordExtension.m */; };
17E0084625941887000C23F0 /* SizeCategories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E0084525941887000C23F0 /* SizeCategories.swift */; };
17E4DBD624BFC53E00FE462A /* AdvancedPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E4DBD524BFC53E00FE462A /* AdvancedPreferencesModel.swift */; };
17EF6A2125C4E5B4002C9F81 /* RSWeb in Frameworks */ = {isa = PBXBuildFile; productRef = 17EF6A2025C4E5B4002C9F81 /* RSWeb */; };
@ -1522,6 +1523,9 @@
17D232A724AFF10A0005F075 /* AddWebFeedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedModel.swift; sourceTree = "<group>"; };
17D3CEE2257C4D2300E74939 /* AddAccountSignUp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountSignUp.swift; sourceTree = "<group>"; };
17D5F17024B0BC6700375168 /* SidebarToolbarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarToolbarModel.swift; sourceTree = "<group>"; };
17D7586C2679C21700B17787 /* NetNewsWire-iOS-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NetNewsWire-iOS-Bridging-Header.h"; sourceTree = "<group>"; };
17D7586D2679C21800B17787 /* OnePasswordExtension.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OnePasswordExtension.h; sourceTree = "<group>"; };
17D7586E2679C21800B17787 /* OnePasswordExtension.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OnePasswordExtension.m; sourceTree = "<group>"; };
17E0084525941887000C23F0 /* SizeCategories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SizeCategories.swift; sourceTree = "<group>"; };
17E4DBD524BFC53E00FE462A /* AdvancedPreferencesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedPreferencesModel.swift; sourceTree = "<group>"; };
3B3A328B238B820900314204 /* FeedWranglerAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerAccountViewController.swift; sourceTree = "<group>"; };
@ -2448,6 +2452,15 @@
path = Add;
sourceTree = "<group>";
};
17D7586B2679C1DF00B17787 /* 1Password */ = {
isa = PBXGroup;
children = (
17D7586D2679C21800B17787 /* OnePasswordExtension.h */,
17D7586E2679C21800B17787 /* OnePasswordExtension.m */,
);
path = 1Password;
sourceTree = "<group>";
};
510289CE2451BA1E00426DDF /* Twitter */ = {
isa = PBXGroup;
children = (
@ -3459,6 +3472,7 @@
510C43F5243D0325009F70C3 /* ExtensionPoints */,
511D43CE231FA51100FB1562 /* Resources */,
176813A22564B9D100D98635 /* Widget */,
17D7586B2679C1DF00B17787 /* 1Password */,
173A64162547BE0900267F6E /* AccountType+Helpers.swift */,
);
path = Shared;
@ -3591,6 +3605,7 @@
51BB7C302335ACDE008E8144 /* page.html */,
514219572353C28900E07E2C /* main_ios.js */,
51C452B72265178500C03939 /* styleSheet.css */,
17D7586C2679C21700B17787 /* NetNewsWire-iOS-Bridging-Header.h */,
84C9FC9B2262A1A900D921D6 /* Assets.xcassets */,
84C9FC9C2262A1A900D921D6 /* Info.plist */,
84BB0F812333426400DED65E /* NetNewsWire.entitlements */,
@ -4125,6 +4140,7 @@
840D617B2029031C009BC708 = {
CreatedOnToolsVersion = 9.3;
DevelopmentTeam = SHJK2V3AJG;
LastSwiftMigration = 1250;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.BackgroundModes = {
@ -5382,6 +5398,7 @@
8454C3F3263F2D8700E3F9C7 /* IconImageCache.swift in Sources */,
B24E9ADE245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */,
C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */,
17D7586F2679C21800B17787 /* OnePasswordExtension.m in Sources */,
51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */,
51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */,
84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */,

View File

@ -0,0 +1,220 @@
//Copyright (c) 2014-2020 AgileBits Inc.
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in all
//copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//SOFTWARE.
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <MobileCoreServices/MobileCoreServices.h>
#ifdef __IPHONE_8_0
#import <WebKit/WebKit.h>
#endif
#if __has_feature(nullability)
NS_ASSUME_NONNULL_BEGIN
#else
#define nullable
#define __nullable
#define nonnull
#define __nonnull
#endif
// Login Dictionary keys - Used to get or set the properties of a 1Password Login
FOUNDATION_EXPORT NSString *const AppExtensionURLStringKey;
FOUNDATION_EXPORT NSString *const AppExtensionUsernameKey;
FOUNDATION_EXPORT NSString *const AppExtensionPasswordKey;
FOUNDATION_EXPORT NSString *const AppExtensionTOTPKey;
FOUNDATION_EXPORT NSString *const AppExtensionTitleKey;
FOUNDATION_EXPORT NSString *const AppExtensionNotesKey;
FOUNDATION_EXPORT NSString *const AppExtensionSectionTitleKey;
FOUNDATION_EXPORT NSString *const AppExtensionFieldsKey;
FOUNDATION_EXPORT NSString *const AppExtensionReturnedFieldsKey;
FOUNDATION_EXPORT NSString *const AppExtensionOldPasswordKey;
FOUNDATION_EXPORT NSString *const AppExtensionPasswordGeneratorOptionsKey;
// Password Generator options - Used to set the 1Password Password Generator options when saving a new Login or when changing the password for for an existing Login
FOUNDATION_EXPORT NSString *const AppExtensionGeneratedPasswordMinLengthKey;
FOUNDATION_EXPORT NSString *const AppExtensionGeneratedPasswordMaxLengthKey;
FOUNDATION_EXPORT NSString *const AppExtensionGeneratedPasswordRequireDigitsKey;
FOUNDATION_EXPORT NSString *const AppExtensionGeneratedPasswordRequireSymbolsKey;
FOUNDATION_EXPORT NSString *const AppExtensionGeneratedPasswordForbiddenCharactersKey;
// Errors codes
FOUNDATION_EXPORT NSString *const AppExtensionErrorDomain;
FOUNDATION_EXPORT NS_ENUM(NSUInteger, AppExtensionErrorCode) {
AppExtensionErrorCodeCancelledByUser = 0,
AppExtensionErrorCodeAPINotAvailable = 1,
AppExtensionErrorCodeFailedToContactExtension = 2,
AppExtensionErrorCodeFailedToLoadItemProviderData = 3,
AppExtensionErrorCodeCollectFieldsScriptFailed = 4,
AppExtensionErrorCodeFillFieldsScriptFailed = 5,
AppExtensionErrorCodeUnexpectedData = 6,
AppExtensionErrorCodeFailedToObtainURLStringFromWebView = 7
};
// Note to creators of libraries or frameworks:
// If you include this code within your library, then to prevent potential duplicate symbol
// conflicts for adopters of your library, you should rename the OnePasswordExtension class
// and associated typedefs. You might to so by adding your own project prefix, e.g.,
// MyLibraryOnePasswordExtension.
typedef void (^OnePasswordLoginDictionaryCompletionBlock)(NSDictionary * __nullable loginDictionary, NSError * __nullable error);
typedef void (^OnePasswordSuccessCompletionBlock)(BOOL success, NSError * __nullable error);
typedef void (^OnePasswordExtensionItemCompletionBlock)(NSExtensionItem * __nullable extensionItem, NSError * __nullable error);
@interface OnePasswordExtension : NSObject
+ (OnePasswordExtension *)sharedExtension;
/*!
@discussion Determines if the 1Password Extension is available. Allows you to only show the 1Password login button to those
that can use it. Of course, you could leave the button enabled and educate users about the virtues of strong, unique
passwords instead :)
@return isAppExtensionAvailable Returns YES if any app that supports the generic `org-appextension-feature-password-management` feature is installed on the device.
*/
#ifdef __IPHONE_8_0
- (BOOL)isAppExtensionAvailable NS_EXTENSION_UNAVAILABLE_IOS("Not available in an extension. Check if org-appextension-feature-password-management:// URL can be opened by the app.");
#else
- (BOOL)isAppExtensionAvailable;
#endif
/*!
Called from your login page, this method will find all available logins for the given URLString.
@discussion 1Password will show all matching Login for the naked domain of the given URLString. For example if the user has an item in your 1Password vault with "subdomain1.domain.com” as the website and another one with "subdomain2.domain.com, and the URLString is "https://domain.com", 1Password will show both items.
However, if no matching login is found for "https://domain.com", the 1Password Extension will display the "Show all Logins" button so that the user can search among all the Logins in the vault. This is especially useful when the user has a login for "https://olddomain.com".
After the user selects a login, it is stored into an NSDictionary and given to your completion handler. Use the `Login Dictionary keys` above to
extract the needed information and update your UI. The completion block is guaranteed to be called on the main thread.
@param URLString For the matching Logins in the 1Password vault.
@param viewController The view controller from which the 1Password Extension is invoked. Usually `self`
@param sender The sender which triggers the share sheet to show. UIButton, UIBarButtonItem or UIView. Can also be nil on iPhone, but not on iPad.
@param completion A completion block called with two parameters loginDictionary and error once completed. The loginDictionary reply parameter that contains the username, password and the One-Time Password if available. The error Reply parameter that is nil if the 1Password Extension has been successfully completed, or it contains error information about the completion failure.
*/
- (void)findLoginForURLString:(nonnull NSString *)URLString forViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender completion:(nonnull OnePasswordLoginDictionaryCompletionBlock)completion;
/*!
Create a new login within 1Password and allow the user to generate a new password before saving.
@discussion The provided URLString should be unique to your app or service and be identical to what you pass into the find login method.
The completion block is guaranteed to be called on the main
thread.
@param URLString For the new Login to be saved in 1Password.
@param loginDetailsDictionary about the Login to be saved, including custom fields, are stored in an dictionary and given to the 1Password Extension.
@param passwordGenerationOptions The Password generator options represented in a dictionary form.
@param viewController The view controller from which the 1Password Extension is invoked. Usually `self`
@param sender The sender which triggers the share sheet to show. UIButton, UIBarButtonItem or UIView. Can also be nil on iPhone, but not on iPad.
@param completion A completion block which is called with type parameters loginDictionary and error. The loginDictionary reply parameter which contain all the information about the newly saved Login. Use the `Login Dictionary keys` above to extract the needed information and update your UI. For example, updating the UI with the newly generated password lets the user know their action was successful. The error reply parameter that is nil if the 1Password Extension has been successfully completed, or it contains error information about the completion failure.
*/
- (void)storeLoginForURLString:(nonnull NSString *)URLString loginDetails:(nullable NSDictionary *)loginDetailsDictionary passwordGenerationOptions:(nullable NSDictionary *)passwordGenerationOptions forViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender completion:(nonnull OnePasswordLoginDictionaryCompletionBlock)completion;
/*!
Change the password for an existing login within 1Password.
@discussion The provided URLString should be unique to your app or service and be identical to what you pass into the find login method. The completion block is guaranteed to be called on the main thread.
1Password 6 and later:
The 1Password Extension will display all available the matching Logins for the given URL string. The user can choose which Login item to update. The "New Login" button will also be available at all times, in case the user wishes to to create a new Login instead,
1Password 5:
These are the three scenarios that are supported:
1. A single matching Login is found: 1Password will enter edit mode for that Login and will update its password using the value for AppExtensionPasswordKey.
2. More than a one matching Logins are found: 1Password will display a list of all matching Logins. The user must choose which one to update. Once in edit mode, the Login will be updated with the new password.
3. No matching login is found: 1Password will create a new Login using the optional fields if available to populate its properties.
@param URLString for the Login to be updated with a new password in 1Password.
@param loginDetailsDictionary about the Login to be saved, including old password and the username, are stored in an dictionary and given to the 1Password Extension.
@param passwordGenerationOptions The Password generator options epresented in a dictionary form.
@param viewController The view controller from which the 1Password Extension is invoked. Usually `self`
@param sender The sender which triggers the share sheet to show. UIButton, UIBarButtonItem or UIView. Can also be nil on iPhone, but not on iPad.
@param completion A completion block which is called with type parameters loginDictionary and error. The loginDictionary reply parameter which contain all the information about the newly updated Login, including the newly generated and the old password. Use the `Login Dictionary keys` above to extract the needed information and update your UI. For example, updating the UI with the newly generated password lets the user know their action was successful. The error reply parameter that is nil if the 1Password Extension has been successfully completed, or it contains error information about the completion failure.
*/
- (void)changePasswordForLoginForURLString:(nonnull NSString *)URLString loginDetails:(nullable NSDictionary *)loginDetailsDictionary passwordGenerationOptions:(nullable NSDictionary *)passwordGenerationOptions forViewController:(UIViewController *)viewController sender:(nullable id)sender completion:(nonnull OnePasswordLoginDictionaryCompletionBlock)completion;
/*!
Called from your web view controller, this method will show all the saved logins for the active page in the provided web
view, and automatically fill the HTML form fields. Supports WKWebView.
@discussion 1Password will show all matching Login for the naked domain of the current website. For example if the user has an item in your 1Password vault with "subdomain1.domain.com” as the website and another one with "subdomain2.domain.com, and the current website is "https://domain.com", 1Password will show both items.
However, if no matching login is found for "https://domain.com", the 1Password Extension will display the "New Login" button so that the user can create a new Login for the current website.
@param webView The web view which displays the form to be filled. The active WKWebView. Must not be nil.
@param viewController The view controller from which the 1Password Extension is invoked. Usually `self`
@param sender The sender which triggers the share sheet to show. UIButton, UIBarButtonItem or UIView. Can also be nil on iPhone, but not on iPad.
@param yesOrNo Boolean flag. If YES is passed only matching Login items will be shown, otherwise the 1Password Extension will also display Credit Cards and Identities.
@param completion Completion block called on completion with parameters success, and error. The success reply parameter that is YES if the 1Password Extension has been successfully completed or NO otherwise. The error reply parameter that is nil if the 1Password Extension has been successfully completed, or it contains error information about the completion failure.
*/
- (void)fillItemIntoWebView:(nonnull WKWebView *)webView forViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender showOnlyLogins:(BOOL)yesOrNo completion:(nonnull OnePasswordSuccessCompletionBlock)completion;
/*!
Called in the UIActivityViewController completion block to find out whether or not the user selected the 1Password Extension activity.
@param activityType or the bundle identidier of the selected activity in the share sheet.
@return isOnePasswordExtensionActivityType Returns YES if the selected activity is the 1Password extension, NO otherwise.
*/
- (BOOL)isOnePasswordExtensionActivityType:(nullable NSString *)activityType;
/*!
The returned NSExtensionItem can be used to create your own UIActivityViewController. Use `isOnePasswordExtensionActivityType:` and `fillReturnedItems:intoWebView:completion:` in the activity view controller completion block to process the result. The completion block is guaranteed to be called on the main thread.
@param webView The web view which displays the form to be filled. The active WKWebView. Must not be nil.
@param completion Completion block called on completion with extensionItem and error. The extensionItem reply parameter that is contains all the info required by the 1Password extension if has been successfully completed or nil otherwise. The error reply parameter that is nil if the 1Password extension item has been successfully created, or it contains error information about the completion failure.
*/
- (void)createExtensionItemForWebView:(nonnull WKWebView *)webView completion:(nonnull OnePasswordExtensionItemCompletionBlock)completion;
/*!
Method used in the UIActivityViewController completion block to fill information into a web view.
@param returnedItems Array which contains the selected activity in the share sheet. Empty array if the share sheet is cancelled by the user.
@param webView The web view which displays the form to be filled. The active WKWebView. Must not be nil.
@param completion Completion block called on completion with parameters success, and error. The success reply parameter that is YES if the 1Password Extension has been successfully completed or NO otherwise. The error reply parameter that is nil if the 1Password Extension has been successfully completed, or it contains error information about the completion failure.
*/
- (void)fillReturnedItems:(nullable NSArray *)returnedItems intoWebView:(nonnull WKWebView *)webView completion:(nonnull OnePasswordSuccessCompletionBlock)completion;
@end
#if __has_feature(nullability)
NS_ASSUME_NONNULL_END
#endif

View File

@ -0,0 +1,638 @@
//Copyright (c) 2014-2020 AgileBits Inc.
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in all
//copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//SOFTWARE.
#import "OnePasswordExtension.h"
NSString *const AppExtensionURLStringKey = @"url_string";
NSString *const AppExtensionUsernameKey = @"username";
NSString *const AppExtensionPasswordKey = @"password";
NSString *const AppExtensionTOTPKey = @"totp";
NSString *const AppExtensionTitleKey = @"login_title";
NSString *const AppExtensionNotesKey = @"notes";
NSString *const AppExtensionSectionTitleKey = @"section_title";
NSString *const AppExtensionFieldsKey = @"fields";
NSString *const AppExtensionReturnedFieldsKey = @"returned_fields";
NSString *const AppExtensionOldPasswordKey = @"old_password";
NSString *const AppExtensionPasswordGeneratorOptionsKey = @"password_generator_options";
NSString *const AppExtensionGeneratedPasswordMinLengthKey = @"password_min_length";
NSString *const AppExtensionGeneratedPasswordMaxLengthKey = @"password_max_length";
NSString *const AppExtensionGeneratedPasswordRequireDigitsKey = @"password_require_digits";
NSString *const AppExtensionGeneratedPasswordRequireSymbolsKey = @"password_require_symbols";
NSString *const AppExtensionGeneratedPasswordForbiddenCharactersKey = @"password_forbidden_characters";
NSString *const AppExtensionErrorDomain = @"OnePasswordExtension";
// Version
#define VERSION_NUMBER @(185)
static NSString *const AppExtensionVersionNumberKey = @"version_number";
// Available App Extension Actions
static NSString *const kUTTypeAppExtensionFindLoginAction = @"org.appextension.find-login-action";
static NSString *const kUTTypeAppExtensionSaveLoginAction = @"org.appextension.save-login-action";
static NSString *const kUTTypeAppExtensionChangePasswordAction = @"org.appextension.change-password-action";
static NSString *const kUTTypeAppExtensionFillWebViewAction = @"org.appextension.fill-webview-action";
static NSString *const kUTTypeAppExtensionFillBrowserAction = @"org.appextension.fill-browser-action";
// WebView Dictionary keys
static NSString *const AppExtensionWebViewPageFillScript = @"fillScript";
static NSString *const AppExtensionWebViewPageDetails = @"pageDetails";
@implementation OnePasswordExtension
#pragma mark - Public Methods
+ (OnePasswordExtension *)sharedExtension {
static dispatch_once_t onceToken;
static OnePasswordExtension *__sharedExtension;
dispatch_once(&onceToken, ^{
__sharedExtension = [OnePasswordExtension new];
});
return __sharedExtension;
}
- (BOOL)isAppExtensionAvailable {
if ([self isSystemAppExtensionAPIAvailable]) {
return [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"org-appextension-feature-password-management://"]];
}
return NO;
}
#pragma mark - Native app Login
- (void)findLoginForURLString:(nonnull NSString *)URLString forViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender completion:(nonnull OnePasswordLoginDictionaryCompletionBlock)completion {
NSAssert(URLString != nil, @"URLString must not be nil");
NSAssert(viewController != nil, @"viewController must not be nil");
if (NO == [self isSystemAppExtensionAPIAvailable]) {
NSLog(@"Failed to findLoginForURLString, system API is not available");
if (completion) {
completion(nil, [OnePasswordExtension systemAppExtensionAPINotAvailableError]);
}
return;
}
NSDictionary *item = @{ AppExtensionVersionNumberKey: VERSION_NUMBER, AppExtensionURLStringKey: URLString };
UIActivityViewController *activityViewController = [self activityViewControllerForItem:item viewController:viewController sender:sender typeIdentifier:kUTTypeAppExtensionFindLoginAction];
activityViewController.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
if (returnedItems.count == 0) {
NSError *error = nil;
if (activityError) {
NSLog(@"Failed to findLoginForURLString: %@", activityError);
error = [OnePasswordExtension failedToContactExtensionErrorWithActivityError:activityError];
}
else {
error = [OnePasswordExtension extensionCancelledByUserError];
}
if (completion) {
completion(nil, error);
}
return;
}
[self processExtensionItem:returnedItems.firstObject completion:^(NSDictionary *itemDictionary, NSError *error) {
if (completion) {
completion(itemDictionary, error);
}
}];
};
[viewController presentViewController:activityViewController animated:YES completion:nil];
}
#pragma mark - New User Registration
- (void)storeLoginForURLString:(nonnull NSString *)URLString loginDetails:(nullable NSDictionary *)loginDetailsDictionary passwordGenerationOptions:(nullable NSDictionary *)passwordGenerationOptions forViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender completion:(nonnull OnePasswordLoginDictionaryCompletionBlock)completion {
NSAssert(URLString != nil, @"URLString must not be nil");
NSAssert(viewController != nil, @"viewController must not be nil");
if (NO == [self isSystemAppExtensionAPIAvailable]) {
NSLog(@"Failed to storeLoginForURLString, system API is not available");
if (completion) {
completion(nil, [OnePasswordExtension systemAppExtensionAPINotAvailableError]);
}
return;
}
NSMutableDictionary *newLoginAttributesDict = [NSMutableDictionary new];
newLoginAttributesDict[AppExtensionVersionNumberKey] = VERSION_NUMBER;
newLoginAttributesDict[AppExtensionURLStringKey] = URLString;
[newLoginAttributesDict addEntriesFromDictionary:loginDetailsDictionary];
if (passwordGenerationOptions.count > 0) {
newLoginAttributesDict[AppExtensionPasswordGeneratorOptionsKey] = passwordGenerationOptions;
}
UIActivityViewController *activityViewController = [self activityViewControllerForItem:newLoginAttributesDict viewController:viewController sender:sender typeIdentifier:kUTTypeAppExtensionSaveLoginAction];
activityViewController.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
if (returnedItems.count == 0) {
NSError *error = nil;
if (activityError) {
NSLog(@"Failed to storeLoginForURLString: %@", activityError);
error = [OnePasswordExtension failedToContactExtensionErrorWithActivityError:activityError];
}
else {
error = [OnePasswordExtension extensionCancelledByUserError];
}
if (completion) {
completion(nil, error);
}
return;
}
[self processExtensionItem:returnedItems.firstObject completion:^(NSDictionary *itemDictionary, NSError *error) {
if (completion) {
completion(itemDictionary, error);
}
}];
};
[viewController presentViewController:activityViewController animated:YES completion:nil];
}
#pragma mark - Change Password
- (void)changePasswordForLoginForURLString:(nonnull NSString *)URLString loginDetails:(nullable NSDictionary *)loginDetailsDictionary passwordGenerationOptions:(nullable NSDictionary *)passwordGenerationOptions forViewController:(UIViewController *)viewController sender:(nullable id)sender completion:(nonnull OnePasswordLoginDictionaryCompletionBlock)completion {
NSAssert(URLString != nil, @"URLString must not be nil");
NSAssert(viewController != nil, @"viewController must not be nil");
if (NO == [self isSystemAppExtensionAPIAvailable]) {
NSLog(@"Failed to changePasswordForLoginWithUsername, system API is not available");
if (completion) {
completion(nil, [OnePasswordExtension systemAppExtensionAPINotAvailableError]);
}
return;
}
NSMutableDictionary *item = [NSMutableDictionary new];
item[AppExtensionVersionNumberKey] = VERSION_NUMBER;
item[AppExtensionURLStringKey] = URLString;
[item addEntriesFromDictionary:loginDetailsDictionary];
if (passwordGenerationOptions.count > 0) {
item[AppExtensionPasswordGeneratorOptionsKey] = passwordGenerationOptions;
}
UIActivityViewController *activityViewController = [self activityViewControllerForItem:item viewController:viewController sender:sender typeIdentifier:kUTTypeAppExtensionChangePasswordAction];
activityViewController.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
if (returnedItems.count == 0) {
NSError *error = nil;
if (activityError) {
NSLog(@"Failed to changePasswordForLoginWithUsername: %@", activityError);
error = [OnePasswordExtension failedToContactExtensionErrorWithActivityError:activityError];
}
else {
error = [OnePasswordExtension extensionCancelledByUserError];
}
if (completion) {
completion(nil, error);
}
return;
}
[self processExtensionItem:returnedItems.firstObject completion:^(NSDictionary *itemDictionary, NSError *error) {
if (completion) {
completion(itemDictionary, error);
}
}];
};
[viewController presentViewController:activityViewController animated:YES completion:nil];
}
#pragma mark - Web View filling Support
- (void)fillItemIntoWebView:(nonnull WKWebView *)webView forViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender showOnlyLogins:(BOOL)yesOrNo completion:(nonnull OnePasswordSuccessCompletionBlock)completion {
NSAssert(webView != nil, @"webView must not be nil");
NSAssert(viewController != nil, @"viewController must not be nil");
NSAssert([webView isKindOfClass:[WKWebView class]], @"webView must be an instance of WKWebView.");
[self fillItemIntoWKWebView:webView forViewController:viewController sender:(id)sender showOnlyLogins:yesOrNo completion:^(BOOL success, NSError *error) {
if (completion) {
completion(success, error);
}
}];
}
#pragma mark - Support for custom UIActivityViewControllers
- (BOOL)isOnePasswordExtensionActivityType:(nullable NSString *)activityType {
return [@"com.agilebits.onepassword-ios.extension" isEqualToString:activityType] || [@"com.agilebits.beta.onepassword-ios.extension" isEqualToString:activityType];
}
- (void)createExtensionItemForWebView:(nonnull WKWebView *)webView completion:(nonnull OnePasswordExtensionItemCompletionBlock)completion {
NSAssert(webView != nil, @"webView must not be nil");
NSAssert([webView isKindOfClass:[WKWebView class]], @"webView must be an instance of WKWebView.");
[webView evaluateJavaScript:OPWebViewCollectFieldsScript completionHandler:^(NSString *result, NSError *evaluateError) {
if (result == nil) {
NSLog(@"1Password Extension failed to collect web page fields: %@", evaluateError);
NSError *failedToCollectFieldsError = [OnePasswordExtension failedToCollectFieldsErrorWithUnderlyingError:evaluateError];
if (completion) {
if ([NSThread isMainThread]) {
completion(nil, failedToCollectFieldsError);
}
else {
dispatch_async(dispatch_get_main_queue(), ^{
completion(nil, failedToCollectFieldsError);
});
}
}
return;
}
[self createExtensionItemForURLString:webView.URL.absoluteString webPageDetails:result completion:completion];
}];
}
- (void)fillReturnedItems:(nullable NSArray *)returnedItems intoWebView:(nonnull WKWebView *)webView completion:(nonnull OnePasswordSuccessCompletionBlock)completion {
NSAssert(webView != nil, @"webView must not be nil");
if (returnedItems.count == 0) {
NSError *error = [OnePasswordExtension extensionCancelledByUserError];
if (completion) {
completion(NO, error);
}
return;
}
[self processExtensionItem:returnedItems.firstObject completion:^(NSDictionary *itemDictionary, NSError *error) {
if (itemDictionary.count == 0) {
if (completion) {
completion(NO, error);
}
return;
}
NSString *fillScript = itemDictionary[AppExtensionWebViewPageFillScript];
[self executeFillScript:fillScript inWebView:webView completion:^(BOOL success, NSError *executeFillScriptError) {
if (completion) {
completion(success, executeFillScriptError);
}
}];
}];
}
#pragma mark - Private methods
- (BOOL)isSystemAppExtensionAPIAvailable {
return [NSExtensionItem class] != nil;
}
- (void)findLoginIn1PasswordWithURLString:(nonnull NSString *)URLString collectedPageDetails:(nullable NSString *)collectedPageDetails forWebViewController:(nonnull UIViewController *)forViewController sender:(nullable id)sender withWebView:(nonnull WKWebView *)webView showOnlyLogins:(BOOL)yesOrNo completion:(nonnull OnePasswordSuccessCompletionBlock)completion {
if ([URLString length] == 0) {
NSError *URLStringError = [OnePasswordExtension failedToObtainURLStringFromWebViewError];
NSLog(@"Failed to findLoginIn1PasswordWithURLString: %@", URLStringError);
if (completion) {
completion(NO, URLStringError);
}
return;
}
NSError *jsonError = nil;
NSData *data = [collectedPageDetails dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *collectedPageDetailsDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&jsonError];
if (collectedPageDetailsDictionary.count == 0) {
NSLog(@"Failed to parse JSON collected page details: %@", jsonError);
if (completion) {
completion(NO, jsonError);
}
return;
}
NSDictionary *item = @{ AppExtensionVersionNumberKey : VERSION_NUMBER, AppExtensionURLStringKey : URLString, AppExtensionWebViewPageDetails : collectedPageDetailsDictionary };
NSString *typeIdentifier = yesOrNo ? kUTTypeAppExtensionFillWebViewAction : kUTTypeAppExtensionFillBrowserAction;
UIActivityViewController *activityViewController = [self activityViewControllerForItem:item viewController:forViewController sender:sender typeIdentifier:typeIdentifier];
activityViewController.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
if (returnedItems.count == 0) {
NSError *error = nil;
if (activityError) {
NSLog(@"Failed to findLoginIn1PasswordWithURLString: %@", activityError);
error = [OnePasswordExtension failedToContactExtensionErrorWithActivityError:activityError];
}
else {
error = [OnePasswordExtension extensionCancelledByUserError];
}
if (completion) {
completion(NO, error);
}
return;
}
[self processExtensionItem:returnedItems.firstObject completion:^(NSDictionary *itemDictionary, NSError *processExtensionItemError) {
if (itemDictionary.count == 0) {
if (completion) {
completion(NO, processExtensionItemError);
}
return;
}
NSString *fillScript = itemDictionary[AppExtensionWebViewPageFillScript];
[self executeFillScript:fillScript inWebView:webView completion:^(BOOL success, NSError *executeFillScriptError) {
if (completion) {
completion(success, executeFillScriptError);
}
}];
}];
};
[forViewController presentViewController:activityViewController animated:YES completion:nil];
}
- (void)fillItemIntoWKWebView:(nonnull WKWebView *)webView forViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender showOnlyLogins:(BOOL)yesOrNo completion:(nonnull OnePasswordSuccessCompletionBlock)completion {
[webView evaluateJavaScript:OPWebViewCollectFieldsScript completionHandler:^(NSString *result, NSError *error) {
if (result == nil) {
NSLog(@"1Password Extension failed to collect web page fields: %@", error);
if (completion) {
completion(NO,[OnePasswordExtension failedToCollectFieldsErrorWithUnderlyingError:error]);
}
return;
}
[self findLoginIn1PasswordWithURLString:webView.URL.absoluteString collectedPageDetails:result forWebViewController:viewController sender:sender withWebView:webView showOnlyLogins:yesOrNo completion:^(BOOL success, NSError *findLoginError) {
if (completion) {
completion(success, findLoginError);
}
}];
}];
}
- (void)executeFillScript:(NSString * __nullable)fillScript inWebView:(nonnull WKWebView *)webView completion:(nonnull OnePasswordSuccessCompletionBlock)completion {
if (fillScript == nil) {
NSLog(@"Failed to executeFillScript, fillScript is missing");
if (completion) {
completion(NO, [OnePasswordExtension failedToFillFieldsErrorWithLocalizedErrorMessage:NSLocalizedStringFromTable(@"Failed to fill web page because script is missing", @"OnePasswordExtension", @"1Password Extension Error Message") underlyingError:nil]);
}
return;
}
NSMutableString *scriptSource = [OPWebViewFillScript mutableCopy];
[scriptSource appendFormat:@"(document, %@, undefined);", fillScript];
[webView evaluateJavaScript:scriptSource completionHandler:^(NSString *result, NSError *evaluationError) {
BOOL success = (result != nil);
NSError *error = nil;
if (!success) {
NSLog(@"Cannot executeFillScript, evaluateJavaScript failed: %@", evaluationError);
error = [OnePasswordExtension failedToFillFieldsErrorWithLocalizedErrorMessage:NSLocalizedStringFromTable(@"Failed to fill web page because script could not be evaluated", @"OnePasswordExtension", @"1Password Extension Error Message") underlyingError:error];
}
if (completion) {
completion(success, error);
}
}];
}
- (void)processExtensionItem:(nullable NSExtensionItem *)extensionItem completion:(nonnull OnePasswordLoginDictionaryCompletionBlock)completion {
if (extensionItem.attachments.count == 0) {
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Unexpected data returned by App Extension: extension item had no attachments." };
NSError *error = [[NSError alloc] initWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeUnexpectedData userInfo:userInfo];
if (completion) {
completion(nil, error);
}
return;
}
NSItemProvider *itemProvider = extensionItem.attachments.firstObject;
if (NO == [itemProvider hasItemConformingToTypeIdentifier:(__bridge NSString *)kUTTypePropertyList]) {
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Unexpected data returned by App Extension: extension item attachment does not conform to kUTTypePropertyList type identifier" };
NSError *error = [[NSError alloc] initWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeUnexpectedData userInfo:userInfo];
if (completion) {
completion(nil, error);
}
return;
}
[itemProvider loadItemForTypeIdentifier:(__bridge NSString *)kUTTypePropertyList options:nil completionHandler:^(NSDictionary *itemDictionary, NSError *itemProviderError) {
NSError *error = nil;
if (itemDictionary.count == 0) {
NSLog(@"Failed to loadItemForTypeIdentifier: %@", itemProviderError);
error = [OnePasswordExtension failedToLoadItemProviderDataErrorWithUnderlyingError:itemProviderError];
}
if (completion) {
if ([NSThread isMainThread]) {
completion(itemDictionary, error);
}
else {
dispatch_async(dispatch_get_main_queue(), ^{
completion(itemDictionary, error);
});
}
}
}];
}
- (UIActivityViewController *)activityViewControllerForItem:(nonnull NSDictionary *)item viewController:(nonnull UIViewController*)viewController sender:(nullable id)sender typeIdentifier:(nonnull NSString *)typeIdentifier {
NSAssert(NO == (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad && sender == nil), @"sender must not be nil on iPad.");
NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithItem:item typeIdentifier:typeIdentifier];
NSExtensionItem *extensionItem = [[NSExtensionItem alloc] init];
extensionItem.attachments = @[ itemProvider ];
UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:@[ extensionItem ] applicationActivities:nil];
if ([sender isKindOfClass:[UIBarButtonItem class]]) {
controller.popoverPresentationController.barButtonItem = sender;
}
else if ([sender isKindOfClass:[UIView class]]) {
controller.popoverPresentationController.sourceView = [sender superview];
controller.popoverPresentationController.sourceRect = [sender frame];
}
else {
NSLog(@"sender can be nil on iPhone");
}
return controller;
}
- (void)createExtensionItemForURLString:(nonnull NSString *)URLString webPageDetails:(nullable NSString *)webPageDetails completion:(nonnull OnePasswordExtensionItemCompletionBlock)completion {
NSError *jsonError = nil;
NSData *data = [webPageDetails dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *webPageDetailsDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&jsonError];
if (webPageDetailsDictionary.count == 0) {
NSLog(@"Failed to parse JSON collected page details: %@", jsonError);
if (completion) {
completion(nil, jsonError);
}
return;
}
NSDictionary *item = @{ AppExtensionVersionNumberKey : VERSION_NUMBER, AppExtensionURLStringKey : URLString, AppExtensionWebViewPageDetails : webPageDetailsDictionary };
NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithItem:item typeIdentifier:kUTTypeAppExtensionFillBrowserAction];
NSExtensionItem *extensionItem = [[NSExtensionItem alloc] init];
extensionItem.attachments = @[ itemProvider ];
if (completion) {
if ([NSThread isMainThread]) {
completion(extensionItem, nil);
}
else {
dispatch_async(dispatch_get_main_queue(), ^{
completion(extensionItem, nil);
});
}
}
}
#pragma mark - Errors
+ (NSError *)systemAppExtensionAPINotAvailableError {
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : NSLocalizedStringFromTable(@"App Extension API is not available in this version of iOS", @"OnePasswordExtension", @"1Password Extension Error Message") };
return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeAPINotAvailable userInfo:userInfo];
}
+ (NSError *)extensionCancelledByUserError {
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : NSLocalizedStringFromTable(@"1Password Extension was cancelled by the user", @"OnePasswordExtension", @"1Password Extension Error Message") };
return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeCancelledByUser userInfo:userInfo];
}
+ (NSError *)failedToContactExtensionErrorWithActivityError:(nullable NSError *)activityError {
NSMutableDictionary *userInfo = [NSMutableDictionary new];
userInfo[NSLocalizedDescriptionKey] = NSLocalizedStringFromTable(@"Failed to contact the 1Password Extension", @"OnePasswordExtension", @"1Password Extension Error Message");
if (activityError) {
userInfo[NSUnderlyingErrorKey] = activityError;
}
return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeFailedToContactExtension userInfo:userInfo];
}
+ (NSError *)failedToCollectFieldsErrorWithUnderlyingError:(nullable NSError *)underlyingError {
NSMutableDictionary *userInfo = [NSMutableDictionary new];
userInfo[NSLocalizedDescriptionKey] = NSLocalizedStringFromTable(@"Failed to execute script that collects web page information", @"OnePasswordExtension", @"1Password Extension Error Message");
if (underlyingError) {
userInfo[NSUnderlyingErrorKey] = underlyingError;
}
return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeCollectFieldsScriptFailed userInfo:userInfo];
}
+ (NSError *)failedToFillFieldsErrorWithLocalizedErrorMessage:(nullable NSString *)errorMessage underlyingError:(nullable NSError *)underlyingError {
NSMutableDictionary *userInfo = [NSMutableDictionary new];
if (errorMessage) {
userInfo[NSLocalizedDescriptionKey] = errorMessage;
}
if (underlyingError) {
userInfo[NSUnderlyingErrorKey] = underlyingError;
}
return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeFillFieldsScriptFailed userInfo:userInfo];
}
+ (NSError *)failedToLoadItemProviderDataErrorWithUnderlyingError:(nullable NSError *)underlyingError {
NSMutableDictionary *userInfo = [NSMutableDictionary new];
userInfo[NSLocalizedDescriptionKey] = NSLocalizedStringFromTable(@"Failed to parse information returned by 1Password Extension", @"OnePasswordExtension", @"1Password Extension Error Message");
if (underlyingError) {
userInfo[NSUnderlyingErrorKey] = underlyingError;
}
return [[NSError alloc] initWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeFailedToLoadItemProviderData userInfo:userInfo];
}
+ (NSError *)failedToObtainURLStringFromWebViewError {
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : NSLocalizedStringFromTable(@"Failed to obtain URL String from web view. The web view must be loaded completely when calling the 1Password Extension", @"OnePasswordExtension", @"1Password Extension Error Message") };
return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeFailedToObtainURLStringFromWebView userInfo:userInfo];
}
#pragma mark - WebView field collection and filling scripts
static NSString *const OPWebViewCollectFieldsScript = @";(function(document, undefined) {\
\
document.addEventListener('input',function(b){!1!==b.isTrusted&&'input'===b.target.tagName.toLowerCase()&&(b.target.dataset['com.agilebits.onepassword.userEdited']='yes')},!0);\
(function(b,a,c){a.FieldCollector=new function(){function f(d){return d?d.toString().toLowerCase():''}function e(d,b,a,e){e!==c&&e===a||null===a||a===c||(d[b]=a)}function k(d,b){var a=[];try{a=d.querySelectorAll(b)}catch(J){console.error('[COLLECT FIELDS] @ag_querySelectorAll Exception in selector \"'+b+'\"')}return a}function m(d){var a,c=[];if(d.labels&&d.labels.length&&0<d.labels.length)c=Array.prototype.slice.call(d.labels);else{d.id&&(c=c.concat(Array.prototype.slice.call(k(b,'label[for='+JSON.stringify(d.id)+\
']'))));if(d.name){a=k(b,'label[for='+JSON.stringify(d.name)+']');for(var e=0;e<a.length;e++)-1===c.indexOf(a[e])&&c.push(a[e])}for(a=d;a&&a!=b;a=a.parentNode)'label'===f(a.tagName)&&-1===c.indexOf(a)&&c.push(a)}0===c.length&&(a=d.parentNode,'dd'===a.tagName.toLowerCase()&&null!==a.previousElementSibling&&'dt'===a.previousElementSibling.tagName.toLowerCase()&&c.push(a.previousElementSibling));return 0<c.length?c.map(function(d){return l(r(d))}).join(''):null}function n(d){var a;for(d=d.parentElement||\
d.parentNode;d&&'td'!=f(d.tagName);)d=d.parentElement||d.parentNode;if(!d||d===c)return null;a=d.parentElement||d.parentNode;if('tr'!=a.tagName.toLowerCase())return null;a=a.previousElementSibling;if(!a||'tr'!=(a.tagName+'').toLowerCase()||a.cells&&d.cellIndex>=a.cells.length)return null;d=r(a.cells[d.cellIndex]);return d=l(d)}function p(a){return a.options?(a=Array.prototype.slice.call(a.options).map(function(a){var d=a.text,d=d?f(d).replace(/\\s/mg,'').replace(/[~`!@$%^&*()\\-_+=:;'\"\\[\\]|\\\\,<.>\\/?]/mg,\
''):null;return[d?d:null,a.value]}),{options:a}):null}function F(a){switch(f(a.type)){case 'checkbox':return a.checked?'':'';case 'hidden':a=a.value;if(!a||'number'!=typeof a.length)return'';254<a.length&&(a=a.substr(0,254)+'...SNIPPED');return a;case 'submit':case 'button':case 'reset':if(''===a.value)return l(r(a))||'';default:return a.value}}function G(a,b){if(-1===['text','password'].indexOf(b.type.toLowerCase())||!(h.test(a.value)||h.test(a.htmlID)||h.test(a.htmlName)||h.test(a.placeholder)||\
h.test(a['label-tag'])||h.test(a['label-data'])||h.test(a['label-aria'])))return!1;if(!a.visible)return!0;if('password'==b.type.toLowerCase())return!1;a=b.type;t(b,!0);return a!==b.type}function H(a){var b={};a.forEach(function(a){b[a.opid]=a});return b}function g(a,b){var c=a[b];if('string'==typeof c)return c;a=a.getAttribute(b);return'string'==typeof a?a:null}function z(a){return'input'===a.nodeName.toLowerCase()&&-1===a.type.search(/button|submit|reset|hidden|checkbox/i)}var u={},h=/((\\b|_|-)pin(\\b|_|-)|password|passwort|kennwort|(\\b|_|-)passe(\\b|_|-)|contraseña|senha||adgangskode|hasło|wachtwoord)/i;\
this.collect=this.a=function(b,c){u={};var d=b.defaultView?b.defaultView:a,h=b.activeElement,E=Array.prototype.slice.call(k(b,'form')).map(function(a,b){var c={};b='__form__'+b;a.opid=b;c.opid=b;e(c,'htmlName',g(a,'name'));e(c,'htmlID',g(a,'id'));b=g(a,'action');b=new URL(b,window.location.href);e(c,'htmlAction',b?b.href:null);e(c,'htmlMethod',g(a,'method'));return c}),D=Array.prototype.slice.call(v(b)).map(function(a,b){z(a)&&a.hasAttribute('value')&&!a.dataset['com.agilebits.onepassword.initialValue']&&\
(a.dataset['com.agilebits.onepassword.initialValue']=a.value);var c={},d='__'+b,q=-1==a.maxLength?999:a.maxLength;if(!q||'number'===typeof q&&isNaN(q))q=999;u[d]=a;a.opid=d;c.opid=d;c.elementNumber=b;e(c,'maxLength',Math.min(q,999),999);c.visible=w(a);c.viewable=x(a);e(c,'htmlID',g(a,'id'));e(c,'htmlName',g(a,'name'));e(c,'htmlClass',g(a,'class'));e(c,'tabindex',g(a,'tabindex'));e(c,'title',g(a,'title'));e(c,'userEdited',!!a.dataset['com.agilebits.onepassword.userEdited']);if('hidden'!=f(a.type)){e(c,\
'label-tag',m(a));e(c,'label-data',g(a,'data-label'));e(c,'label-aria',g(a,'aria-label'));e(c,'label-top',n(a));b=[];for(d=a;d&&d.nextSibling;){d=d.nextSibling;if(y(d))break;A(b,d)}e(c,'label-right',b.join(''));b=[];B(a,b);b=b.reverse().join('');e(c,'label-left',b);e(c,'placeholder',g(a,'placeholder'))}e(c,'rel',g(a,'rel'));e(c,'type',f(g(a,'type')));e(c,'value',F(a));e(c,'checked',a.checked,!1);e(c,'autoCompleteType',a.getAttribute('x-autocompletetype')||a.getAttribute('autocompletetype')||a.getAttribute('autocomplete'),\
'off');e(c,'disabled',a.disabled);e(c,'readonly',a.c||a.readOnly);e(c,'selectInfo',p(a));e(c,'aria-hidden','true'==a.getAttribute('aria-hidden'),!1);e(c,'aria-disabled','true'==a.getAttribute('aria-disabled'),!1);e(c,'aria-haspopup','true'==a.getAttribute('aria-haspopup'),!1);e(c,'data-unmasked',a.dataset.unmasked);e(c,'data-stripe',g(a,'data-stripe'));e(c,'data-braintree-name',g(a,'data-braintree-name'));e(c,'onepasswordFieldType',a.dataset.onepasswordFieldType||a.type);e(c,'onepasswordDesignation',\
a.dataset.onepasswordDesignation);e(c,'onepasswordSignInUrl',a.dataset.onepasswordSignInUrl);e(c,'onepasswordSectionTitle',a.dataset.onepasswordSectionTitle);e(c,'onepasswordSectionFieldKind',a.dataset.onepasswordSectionFieldKind);e(c,'onepasswordSectionFieldTitle',a.dataset.onepasswordSectionFieldTitle);e(c,'onepasswordSectionFieldValue',a.dataset.onepasswordSectionFieldValue);a.form&&(c.form=g(a.form,'opid'));e(c,'fakeTested',G(c,a),!1);return c});D.filter(function(a){return a.fakeTested}).forEach(function(a){var b=\
u[a.opid];b.getBoundingClientRect();var c=b.value;t(b,!1);b.dispatchEvent(C(b,'keydown'));b.dispatchEvent(C(b,'keypress'));b.dispatchEvent(C(b,'keyup'));if(''===b.value||b.dataset['com.agilebits.onepassword.initialValue']&&b.value===b.dataset['com.agilebits.onepassword.initialValue'])b.value=c;b.click&&b.click();a.postFakeTestVisible=w(b);a.postFakeTestViewable=x(b);a.postFakeTestType=b.type;a=b.value;var c=b.ownerDocument.createEvent('HTMLEvents'),d=b.ownerDocument.createEvent('HTMLEvents');b.dispatchEvent(C(b,\
'keydown'));b.dispatchEvent(C(b,'keypress'));b.dispatchEvent(C(b,'keyup'));d.initEvent('input',!0,!0);b.dispatchEvent(d);c.initEvent('change',!0,!0);b.dispatchEvent(c);b.blur();if(''===b.value||b.dataset['com.agilebits.onepassword.initialValue']&&b.value===b.dataset['com.agilebits.onepassword.initialValue'])b.value=a});c={documentUUID:c,title:b.title,url:d.location.href,documentURL:b.location.href,forms:H(E),fields:D,collectedTimestamp:(new Date).getTime()};(b=b.querySelector('[data-onepassword-title]'))&&\
b.dataset.onepasswordTitle&&(c.displayTitle=b.dataset.onepasswordTitle);h&&z(h)&&t(h,!0);return c};this.elementForOPID=this.b=function(a){return u[a]}}})(document,window,void 0);document.elementForOPID=I;function C(b,a){var c;c=b.ownerDocument.createEvent('Events');c.initEvent(a,!0,!1);c.charCode=0;c.keyCode=0;c.which=0;c.srcElement=b;c.target=b;return c}window.LOGIN_TITLES=[/^\\W*log\\W*[oi]n\\W*$/i,/log\\W*[oi]n (?:securely|now)/i,/^\\W*sign\\W*[oi]n\\W*$/i,'continue','submit','weiter','accès','вход','connexion','entrar','anmelden','accedi','valider','',' '];window.CHANGE_PASSWORD_TITLES=[/^(change|update) password$/i,'save changes','update'];\
window.LOGIN_RED_HERRING_TITLES=['already have an account','sign in with'];window.REGISTER_TITLES=['register','sign up','signup','join',/^create (my )?(account|profile)$/i,'регистрация','inscription','regístrate','cadastre-se','registrieren','registrazione','',' '];window.SEARCH_TITLES='search find поиск найти искать recherche suchen buscar suche ricerca procurar '.split(' ');window.FORGOT_PASSWORD_TITLES='forgot geändert vergessen hilfe changeemail español'.split(' ');\
window.REMEMBER_ME_TITLES=['remember me','rememberme','keep me signed in'];window.BACK_TITLES=['back','назад'];window.DIVITIS_BUTTON_CLASSES=['button','btn-primary'];function r(b){return b.textContent||b.innerText}function l(b){var a=null;b&&(a=b.replace(/^\\s+|\\s+$|\\r?\\n.*$/mg,'').replace(/\\s{2,}/,' '),a=0<a.length?a:null);return a}function A(b,a){var c='';3===a.nodeType?c=a.nodeValue:1===a.nodeType&&(c=r(a));(a=l(c))&&b.push(a)}\
function y(b){var a;b&&void 0!==b?(a='select option input form textarea button table iframe body head script'.split(' '),b?(b=b?(b.tagName||'').toLowerCase():'',a=a.constructor==Array?0<=a.indexOf(b):b===a):a=!1):a=!0;return a}\
function B(b,a,c){var f;for(c||(c=0);b&&b.previousSibling;){b=b.previousSibling;if(y(b))return;A(a,b)}if(b&&0===a.length){for(f=null;!f;){b=b.parentElement||b.parentNode;if(!b)return;for(f=b.previousSibling;f&&!y(f)&&f.lastChild;)f=f.lastChild}y(f)||(A(a,f),0===a.length&&B(f,a,c+1))}}\
function w(b){for(var a=b,c=(b=b.ownerDocument)?b.defaultView:{},f;a&&a!==b;){f=c.getComputedStyle&&a instanceof Element?c.getComputedStyle(a,null):a.style;if(!f)return!0;if('none'===f.display||'hidden'==f.visibility)return!1;a=a.parentNode}return a===b}\
function x(b){var a=b.ownerDocument.documentElement,c=b.getBoundingClientRect(),f=a.scrollWidth,e=a.scrollHeight,k=c.left-a.clientLeft,a=c.top-a.clientTop,m;if(!w(b)||!b.offsetParent||10>b.clientWidth||10>b.clientHeight)return!1;var n=b.getClientRects();if(0===n.length)return!1;for(var p=0;p<n.length;p++)if(m=n[p],m.left>f||0>m.right)return!1;if(0>k||k>f||0>a||a>e)return!1;for(c=b.ownerDocument.elementFromPoint(k+(c.right>window.innerWidth?(window.innerWidth-k)/2:c.width/2),a+(c.bottom>window.innerHeight?\
(window.innerHeight-a)/2:c.height/2));c&&c!==b&&c!==document;){if(c.tagName&&'string'===typeof c.tagName&&'label'===c.tagName.toLowerCase()&&b.labels&&0<b.labels.length)return 0<=Array.prototype.slice.call(b.labels).indexOf(c);c=c.parentNode}return c===b}\
function I(b){var a;if(void 0===b||null===b)return null;if(a=FieldCollector.b(b))return a;try{var c=Array.prototype.slice.call(v(document)),f=c.filter(function(a){return a.opid==b});if(0<f.length)a=f[0],1<f.length&&console.warn('More than one element found with opid '+b);else{var e=parseInt(b.split('__')[1],10);isNaN(e)||(a=c[e])}}catch(k){console.error('An unexpected error occurred: '+k)}finally{return a}};function v(b){var a=[];try{a=b.querySelectorAll('input, select, button')}catch(c){console.error('[COMMON] @ag_querySelectorAll Exception in selector \"input, select, button\"')}return a}function t(b,a){if(b){var c;a&&(c=b.value);'function'===typeof b.click&&b.click();'function'===typeof b.focus&&b.focus();a&&b.value!==c&&(b.value=c)}};\
\
return JSON.stringify(FieldCollector.a(document, 'oneshotUUID'));\
})(document);\
\
";
static NSString *const OPWebViewFillScript = @";(function(document, fillScript, undefined) {\
\
var g=!0,h=!0,k=!0;function m(a){return a?0===a.indexOf('https://')&&'http:'===document.location.protocol&&(a=document.querySelectorAll('input[type=password]'),0<a.length&&(confirmResult=confirm('1Password warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page.\\n\\nDo you still wish to fill this login?'),0==confirmResult))?!0:!1:!1}\
function l(a){var b,c=[],d=a.properties,e=1,f=[];d&&d.delay_between_operations&&(e=d.delay_between_operations);if(!m(a.savedURL)){var r=function(a,b){var c=a[0];if(void 0===c)b();else{if('delay'===c.operation||'delay'===c[0])e=c.parameters?c.parameters[0]:c[1];else if(c=n(c))for(var d=0;d<c.length;d++)-1===f.indexOf(c[d])&&f.push(c[d]);setTimeout(function(){r(a.slice(1),b)},e)}};g=k=!0;if(b=a.options)b.hasOwnProperty('animate')&&(h=b.animate),b.hasOwnProperty('markFilling')&&(g=b.markFilling);if((b=\
a.metadata)&&b.hasOwnProperty('action'))switch(b.action){case 'fillPassword':g=!1;break;case 'fillLogin':k=!1}a.hasOwnProperty('script')&&r(a.script,function(){a.hasOwnProperty('autosubmit')&&'function'==typeof autosubmit&&(a.itemType&&'fillLogin'!==a.itemType||(0<f.length?setTimeout(function(){autosubmit(a.autosubmit,d.allow_clicky_autosubmit,f)},AUTOSUBMIT_DELAY):DEBUG_AUTOSUBMIT&&console.log('[AUTOSUBMIT] Not attempting to submit since no fields were filled: ',f)));c=f.map(function(a){return a&&\
a.hasOwnProperty('opid')?a.opid:null});'object'==typeof protectedGlobalPage&&protectedGlobalPage.c('fillItemResults',{documentUUID:documentUUID,fillContextIdentifier:a.fillContextIdentifier,usedOpids:c},function(){fillingItemType=null})})}}var y={fill_by_opid:p,fill_by_query:q,click_on_opid:t,click_on_query:u,touch_all_fields:v,simple_set_value_by_query:w,focus_by_opid:x,delay:null};\
function n(a){var b;if(a.hasOwnProperty('operation')&&a.hasOwnProperty('parameters'))b=a.operation,a=a.parameters;else if('[object Array]'===Object.prototype.toString.call(a))b=a[0],a=a.splice(1);else return null;return y.hasOwnProperty(b)?y[b].apply(this,a):null}function p(a,b){return(a=z(a))?(A(a,b),[a]):null}function q(a,b){a=B(a);return Array.prototype.map.call(Array.prototype.slice.call(a),function(a){A(a,b);return a},this)}\
function w(a,b){var c=[];a=B(a);Array.prototype.forEach.call(Array.prototype.slice.call(a),function(a){a.disabled||a.a||a.readOnly||void 0===a.value||(a.value=b,c.push(a))});return c}function x(a){(a=z(a))&&C(a,!0);return null}function t(a){return(a=z(a))?C(a,!1)?[a]:null:null}function u(a){a=B(a);return Array.prototype.map.call(Array.prototype.slice.call(a),function(a){C(a,!0);return[a]},this)}function v(){D()};var E={'true':!0,y:!0,1:!0,yes:!0,'':!0},F=200;function A(a,b){var c;if(!(!a||null===b||void 0===b||k&&(a.disabled||a.a||a.readOnly)))switch(g&&!a.opfilled&&(a.opfilled=!0,a.form&&(a.form.opfilled=!0)),a.type?a.type.toLowerCase():null){case 'checkbox':c=b&&1<=b.length&&E.hasOwnProperty(b.toLowerCase())&&!0===E[b.toLowerCase()];a.checked===c||G(a,function(a){a.checked=c});break;case 'radio':!0===E[b.toLowerCase()]&&a.click();break;default:a.value==b||G(a,function(a){a.value=b})}}\
function G(a,b){H(a);b(a);I(a);J(a)&&(a.className+=' com-agilebits-onepassword-extension-animated-fill',setTimeout(function(){a&&a.className&&(a.className=a.className.replace(/(\\s)?com-agilebits-onepassword-extension-animated-fill/,''))},F))};document.elementForOPID=z;function K(a,b){var c;c=a.ownerDocument.createEvent('Events');c.initEvent(b,!0,!1);c.charCode=0;c.keyCode=0;c.which=0;c.srcElement=a;c.target=a;return c}function H(a){var b=a.value;C(a,!1);a.dispatchEvent(K(a,'keydown'));a.dispatchEvent(K(a,'keypress'));a.dispatchEvent(K(a,'keyup'));if(''===a.value||a.dataset['com.agilebits.onepassword.initialValue']&&a.value===a.dataset['com.agilebits.onepassword.initialValue'])a.value=b}\
function I(a){var b=a.value,c=a.ownerDocument.createEvent('HTMLEvents'),d=a.ownerDocument.createEvent('HTMLEvents');a.dispatchEvent(K(a,'keydown'));a.dispatchEvent(K(a,'keypress'));a.dispatchEvent(K(a,'keyup'));d.initEvent('input',!0,!0);a.dispatchEvent(d);c.initEvent('change',!0,!0);a.dispatchEvent(c);a.blur();if(''===a.value||a.dataset['com.agilebits.onepassword.initialValue']&&a.value===a.dataset['com.agilebits.onepassword.initialValue'])a.value=b}\
function L(){var a=/((\\b|_|-)pin(\\b|_|-)|password|passwort|kennwort|passe|contraseña|senha||adgangskode|hasło|wachtwoord)/i;return Array.prototype.slice.call(B(\"input[type='text']\")).filter(function(b){return b.value&&a.test(b.value)},this)}function D(){L().forEach(function(a){H(a);a.click&&a.click();I(a)})}\
window.LOGIN_TITLES=[/^\\W*log\\W*[oi]n\\W*$/i,/log\\W*[oi]n (?:securely|now)/i,/^\\W*sign\\W*[oi]n\\W*$/i,'continue','submit','weiter','accès','вход','connexion','entrar','anmelden','accedi','valider','',' '];window.CHANGE_PASSWORD_TITLES=[/^(change|update) password$/i,'save changes','update'];window.LOGIN_RED_HERRING_TITLES=['already have an account','sign in with'];\
window.REGISTER_TITLES=['register','sign up','signup','join',/^create (my )?(account|profile)$/i,'регистрация','inscription','regístrate','cadastre-se','registrieren','registrazione','',' '];window.SEARCH_TITLES='search find поиск найти искать recherche suchen buscar suche ricerca procurar '.split(' ');window.FORGOT_PASSWORD_TITLES='forgot geändert vergessen hilfe changeemail español'.split(' ');window.REMEMBER_ME_TITLES=['remember me','rememberme','keep me signed in'];\
window.BACK_TITLES=['back','назад'];window.DIVITIS_BUTTON_CLASSES=['button','btn-primary'];function J(a){var b;if(b=h)a:{b=a;for(var c=a.ownerDocument,d=c?c.defaultView:{},e;b&&b!==c;){e=d.getComputedStyle&&b instanceof Element?d.getComputedStyle(b,null):b.style;if(!e){b=!0;break a}if('none'===e.display||'hidden'==e.visibility){b=!1;break a}b=b.parentNode}b=b===c}return b?-1!=='email text password number tel url'.split(' ').indexOf(a.type||''):!1}\
function z(a){var b;if(void 0===a||null===a)return null;if(b=FieldCollector.b(a))return b;try{var c=Array.prototype.slice.call(B('input, select, button')),d=c.filter(function(b){return b.opid==a});if(0<d.length)b=d[0],1<d.length&&console.warn('More than one element found with opid '+a);else{var e=parseInt(a.split('__')[1],10);isNaN(e)||(b=c[e])}}catch(f){console.error('An unexpected error occurred: '+f)}finally{return b}};function B(a){var b=document,c=[];try{c=b.querySelectorAll(a)}catch(d){console.error('[COMMON] @ag_querySelectorAll Exception in selector \"'+a+'\"')}return c}function C(a,b){if(!a)return!1;var c;b&&(c=a.value);'function'===typeof a.click&&a.click();'function'===typeof a.focus&&a.focus();b&&a.value!==c&&(a.value=c);return'function'===typeof a.click||'function'===typeof a.focus};\
\
l(fillScript);\
return JSON.stringify({'success': true});\
})\
\
";
@end

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17505"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -53,7 +53,7 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="6Ik-WC-e74">
<rect key="frame" x="20" y="8" width="373" height="78.5"/>
<rect key="frame" x="20" y="8" width="373" height="65.5"/>
<string key="text">Sign in to your Feed Wranger account to sync your subscriptions across your devices. Your username and password will be encrypted and stored in Keychain.
Don't have a Feed Wrangler account?</string>
@ -62,7 +62,7 @@ Don't have a Feed Wrangler account?</string>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="8ez-iV-55B">
<rect key="frame" x="169.5" y="85" width="75" height="27"/>
<rect key="frame" x="172" y="72" width="70" height="26"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<state key="normal" title="Sign Up Here"/>
<connections>
@ -90,7 +90,7 @@ Don't have a Feed Wrangler account?</string>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Email" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="o06-fe-i3S">
<rect key="frame" x="20" y="11" width="334" height="22"/>
<rect key="frame" x="20" y="13" width="334" height="18.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" spellCheckingType="no" keyboardType="emailAddress" textContentType="username"/>
</textField>
@ -110,12 +110,12 @@ Don't have a Feed Wrangler account?</string>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Password" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="Z6i-nX-CwJ">
<rect key="frame" x="20" y="11.5" width="284" height="21"/>
<rect key="frame" x="20" y="13.5" width="290" height="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" secureTextEntry="YES" textContentType="password"/>
</textField>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="lBg-Pn-8ao">
<rect key="frame" x="312" y="5.5" width="42" height="33"/>
<rect key="frame" x="318" y="7.5" width="36" height="29"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<state key="normal" title="Show"/>
<connections>
@ -178,19 +178,31 @@ Don't have a Feed Wrangler account?</string>
<action selector="cancel:" destination="fPs-Pp-Qk4" id="C0j-OR-yQ2"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" id="ojY-jN-yDr">
<view key="customView" contentMode="scaleToFill" id="6k3-VP-uPP">
<rect key="frame" x="374" y="12" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="mVm-hL-hqw">
<rect key="frame" x="36" y="6" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</barButtonItem>
<rightBarButtonItems>
<barButtonItem title="Item" image="1password" id="pyb-oi-0r2" userLabel="1Password">
<connections>
<action selector="retrievePasswordDetailsFrom1Password:" destination="fPs-Pp-Qk4" id="DpE-Cr-s5P"/>
</connections>
</barButtonItem>
<barButtonItem id="ojY-jN-yDr">
<view key="customView" contentMode="scaleToFill" id="6k3-VP-uPP">
<rect key="frame" x="332" y="12" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="mVm-hL-hqw">
<rect key="frame" x="0.0" y="0.0" width="20" height="20"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="mVm-hL-hqw" firstAttribute="centerX" secondItem="6k3-VP-uPP" secondAttribute="centerX" id="Qcn-ZS-MPs"/>
<constraint firstItem="mVm-hL-hqw" firstAttribute="height" secondItem="6k3-VP-uPP" secondAttribute="height" id="WXs-IU-eDJ"/>
<constraint firstItem="mVm-hL-hqw" firstAttribute="centerY" secondItem="6k3-VP-uPP" secondAttribute="centerY" id="b44-z7-NiN"/>
<constraint firstItem="mVm-hL-hqw" firstAttribute="width" secondItem="6k3-VP-uPP" secondAttribute="width" id="kho-qu-9Yf"/>
</constraints>
</view>
</barButtonItem>
</rightBarButtonItems>
</navigationItem>
<connections>
<outlet property="actionButton" destination="kKc-mk-vsU" id="TXr-cm-Oyp"/>
@ -198,6 +210,7 @@ Don't have a Feed Wrangler account?</string>
<outlet property="cancelBarButtonItem" destination="zbP-iL-kfC" id="TT3-iu-IvG"/>
<outlet property="emailTextField" destination="o06-fe-i3S" id="WHW-3E-trH"/>
<outlet property="footerLabel" destination="6Ik-WC-e74" id="KwB-tD-kTN"/>
<outlet property="onepasswordButton" destination="pyb-oi-0r2" id="keC-RI-1l5"/>
<outlet property="passwordTextField" destination="Z6i-nX-CwJ" id="p36-53-RsD"/>
<outlet property="showHideButton" destination="lBg-Pn-8ao" id="GgE-Nx-gFL"/>
</connections>
@ -234,7 +247,7 @@ Don't have a Feed Wrangler account?</string>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Local accounts do not sync your subscriptions across devices." textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5ce-ZL-glQ">
<rect key="frame" x="20" y="8" width="373" height="14.5"/>
<rect key="frame" x="20" y="8" width="373" height="13.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
@ -258,7 +271,7 @@ Don't have a Feed Wrangler account?</string>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Name" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="Yl1-R6-xZi">
<rect key="frame" x="20" y="11" width="334" height="22"/>
<rect key="frame" x="20" y="13" width="334" height="18.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" autocapitalizationType="words"/>
</textField>
@ -338,7 +351,7 @@ Don't have a Feed Wrangler account?</string>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sgL-0C-JZa">
<rect key="frame" x="20" y="8" width="373" height="78.5"/>
<rect key="frame" x="20" y="8" width="373" height="65.5"/>
<string key="text">Sign in to your Feedbin account to sync your subscriptions across your devices. Your username and password will be encrypted and stored in Keychain.
Don't have a Feedbin account?</string>
@ -347,7 +360,7 @@ Don't have a Feedbin account?</string>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Xhf-bK-vzm">
<rect key="frame" x="169.5" y="85" width="75" height="27"/>
<rect key="frame" x="172" y="72" width="70" height="26"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<state key="normal" title="Sign Up Here"/>
<connections>
@ -375,7 +388,7 @@ Don't have a Feedbin account?</string>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Email" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="vJa-NN-yjR">
<rect key="frame" x="20" y="11" width="334" height="22"/>
<rect key="frame" x="20" y="13" width="334" height="18.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" spellCheckingType="no" keyboardType="emailAddress" textContentType="username"/>
</textField>
@ -395,12 +408,12 @@ Don't have a Feedbin account?</string>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Password" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="YC2-RH-QoV">
<rect key="frame" x="20" y="11.5" width="284" height="21"/>
<rect key="frame" x="20" y="13.5" width="290" height="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" secureTextEntry="YES" textContentType="password"/>
</textField>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TfW-wf-V06">
<rect key="frame" x="312" y="5.5" width="42" height="33"/>
<rect key="frame" x="318" y="7.5" width="36" height="29"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<state key="normal" title="Show"/>
<connections>
@ -463,19 +476,31 @@ Don't have a Feedbin account?</string>
<action selector="cancel:" destination="ECy-jg-Kyc" id="ZKI-gV-ylg"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" id="Xwp-LO-qff">
<view key="customView" contentMode="scaleToFill" id="cn4-b1-uZa">
<rect key="frame" x="374" y="12" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="YvV-hB-lzT">
<rect key="frame" x="36" y="6" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</barButtonItem>
<rightBarButtonItems>
<barButtonItem title="Item" image="1password" id="VG1-ck-a7f" userLabel="1password">
<connections>
<action selector="retrievePasswordDetailsFrom1Password:" destination="ECy-jg-Kyc" id="nIB-Y6-hqa"/>
</connections>
</barButtonItem>
<barButtonItem id="Xwp-LO-qff">
<view key="customView" contentMode="scaleToFill" id="cn4-b1-uZa">
<rect key="frame" x="332" y="12" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="YvV-hB-lzT">
<rect key="frame" x="0.0" y="0.0" width="20" height="20"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="YvV-hB-lzT" firstAttribute="centerY" secondItem="cn4-b1-uZa" secondAttribute="centerY" id="8Lw-iQ-aCu"/>
<constraint firstItem="YvV-hB-lzT" firstAttribute="centerX" secondItem="cn4-b1-uZa" secondAttribute="centerX" id="KjS-KK-WVn"/>
<constraint firstItem="YvV-hB-lzT" firstAttribute="width" secondItem="cn4-b1-uZa" secondAttribute="width" id="Ybl-fy-acr"/>
<constraint firstItem="YvV-hB-lzT" firstAttribute="height" secondItem="cn4-b1-uZa" secondAttribute="height" id="aUy-B8-FqB"/>
</constraints>
</view>
</barButtonItem>
</rightBarButtonItems>
</navigationItem>
<connections>
<outlet property="actionButton" destination="gv7-yG-aE3" id="ENc-5A-hQc"/>
@ -483,6 +508,7 @@ Don't have a Feedbin account?</string>
<outlet property="cancelBarButtonItem" destination="pfF-Of-5NT" id="Zr3-qD-1Yi"/>
<outlet property="emailTextField" destination="vJa-NN-yjR" id="nCF-9W-YsF"/>
<outlet property="footerLabel" destination="sgL-0C-JZa" id="b6I-Mk-2K3"/>
<outlet property="onepasswordButton" destination="VG1-ck-a7f" id="qJi-3O-8Ru"/>
<outlet property="passwordTextField" destination="YC2-RH-QoV" id="qaX-0i-7jq"/>
<outlet property="showHideButton" destination="TfW-wf-V06" id="PbL-67-Nrg"/>
</connections>
@ -519,7 +545,7 @@ Don't have a Feedbin account?</string>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fal-e8-3BB">
<rect key="frame" x="20" y="8" width="373" height="78.5"/>
<rect key="frame" x="20" y="8" width="373" height="65.5"/>
<string key="text">Sign in to your NewsBlur account to sync your subscriptions across your devices. Your username and password will be encrypted and stored in Keychain.
Don't have a NewsBlur account?</string>
@ -528,7 +554,7 @@ Don't have a NewsBlur account?</string>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="YhB-G0-eeJ">
<rect key="frame" x="169.5" y="85" width="75" height="27"/>
<rect key="frame" x="172" y="72" width="70" height="26"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<state key="normal" title="Sign Up Here"/>
<connections>
@ -556,7 +582,7 @@ Don't have a NewsBlur account?</string>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Username or Email" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="S4v-fs-DIO">
<rect key="frame" x="20" y="11" width="334" height="22"/>
<rect key="frame" x="20" y="13" width="334" height="18.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" spellCheckingType="no" keyboardType="emailAddress" textContentType="username"/>
</textField>
@ -576,12 +602,12 @@ Don't have a NewsBlur account?</string>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Password" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="fct-XR-fEa">
<rect key="frame" x="20" y="11.5" width="284" height="21"/>
<rect key="frame" x="20" y="13.5" width="290" height="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" secureTextEntry="YES" textContentType="password"/>
</textField>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="GY9-nr-jFb">
<rect key="frame" x="312" y="5.5" width="42" height="33"/>
<rect key="frame" x="318" y="7.5" width="36" height="29"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<state key="normal" title="Show"/>
<connections>
@ -644,25 +670,38 @@ Don't have a NewsBlur account?</string>
<action selector="cancel:" destination="Cge-ND-NpD" id="9zR-LJ-IWk"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" id="4yi-H0-B9J">
<view key="customView" contentMode="scaleToFill" id="8DU-L0-P6c">
<rect key="frame" x="374" y="12" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="HfW-jV-MjK">
<rect key="frame" x="36" y="6" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</barButtonItem>
<rightBarButtonItems>
<barButtonItem title="Item" image="1password" id="dVp-Gh-NdH">
<connections>
<action selector="retrievePasswordDetailsFrom1Password:" destination="Cge-ND-NpD" id="1Gz-2G-7js"/>
</connections>
</barButtonItem>
<barButtonItem id="4yi-H0-B9J">
<view key="customView" contentMode="scaleToFill" id="8DU-L0-P6c">
<rect key="frame" x="332" y="12" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="HfW-jV-MjK">
<rect key="frame" x="0.0" y="0.0" width="20" height="20"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="HfW-jV-MjK" firstAttribute="centerX" secondItem="8DU-L0-P6c" secondAttribute="centerX" id="6ly-W7-21T"/>
<constraint firstItem="HfW-jV-MjK" firstAttribute="height" secondItem="8DU-L0-P6c" secondAttribute="height" id="Yag-ad-Ypu"/>
<constraint firstItem="HfW-jV-MjK" firstAttribute="width" secondItem="8DU-L0-P6c" secondAttribute="width" id="mcl-kw-rNk"/>
<constraint firstItem="HfW-jV-MjK" firstAttribute="centerY" secondItem="8DU-L0-P6c" secondAttribute="centerY" id="w1q-rr-rol"/>
</constraints>
</view>
</barButtonItem>
</rightBarButtonItems>
</navigationItem>
<connections>
<outlet property="actionButton" destination="E1I-C4-JdL" id="q2T-4o-c8i"/>
<outlet property="activityIndicator" destination="HfW-jV-MjK" id="AIV-uG-9uC"/>
<outlet property="cancelBarButtonItem" destination="bl6-Y1-wQ8" id="ohR-gW-5J2"/>
<outlet property="footerLabel" destination="fal-e8-3BB" id="7Fq-Oz-aEx"/>
<outlet property="onepasswordButton" destination="dVp-Gh-NdH" id="lYf-yV-GgW"/>
<outlet property="passwordTextField" destination="fct-XR-fEa" id="fGL-4k-gZ6"/>
<outlet property="showHideButton" destination="GY9-nr-jFb" id="1p9-9F-GMY"/>
<outlet property="usernameTextField" destination="S4v-fs-DIO" id="B7I-yz-M0T"/>
@ -684,7 +723,7 @@ Don't have a NewsBlur account?</string>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7Jj-p8-lYw">
<rect key="frame" x="20" y="8" width="373" height="62.5"/>
<rect key="frame" x="20" y="8" width="373" height="52.5"/>
<string key="text">Use your Reader account to sync your subscriptions across your devices.
Don't have a Reader account?</string>
@ -693,7 +732,7 @@ Don't have a Reader account?</string>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="3Fq-U4-PS5">
<rect key="frame" x="169.5" y="69" width="75" height="27"/>
<rect key="frame" x="172" y="59" width="70" height="26"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<state key="normal" title="Sign Up Here"/>
<connections>
@ -721,7 +760,7 @@ Don't have a Reader account?</string>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Username or Email" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="CZg-x8-936">
<rect key="frame" x="14" y="11" width="347" height="22"/>
<rect key="frame" x="14" y="13" width="347" height="18.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" spellCheckingType="no" keyboardType="emailAddress" textContentType="username"/>
</textField>
@ -741,12 +780,12 @@ Don't have a Reader account?</string>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Password" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="KgN-kQ-Cyc">
<rect key="frame" x="14" y="11.5" width="294" height="21"/>
<rect key="frame" x="14" y="13.5" width="300" height="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" secureTextEntry="YES" textContentType="password"/>
</textField>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="cFF-qt-WLs">
<rect key="frame" x="316" y="5.5" width="42" height="33"/>
<rect key="frame" x="322" y="7.5" width="36" height="29"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<state key="normal" title="Show"/>
<connections>
@ -772,7 +811,7 @@ Don't have a Reader account?</string>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="API URL" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="iPv-M2-U8Q">
<rect key="frame" x="14" y="11" width="340" height="22"/>
<rect key="frame" x="14" y="11" width="340" height="18.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardType="URL" smartDashesType="no" smartInsertDeleteType="no" smartQuotesType="no" textContentType="url"/>
</textField>
@ -825,19 +864,31 @@ Don't have a Reader account?</string>
<action selector="cancel:" destination="MzG-hS-TpF" id="a49-Fh-i1S"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" id="Ih6-jI-jFg">
<view key="customView" contentMode="scaleToFill" id="gSl-PT-7DH">
<rect key="frame" x="374" y="12" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="pdn-6v-d9a">
<rect key="frame" x="36" y="6" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</barButtonItem>
<rightBarButtonItems>
<barButtonItem title="Item" image="1password" id="MLD-vW-VQN">
<connections>
<action selector="retrievePasswordDetailsFrom1Password:" destination="MzG-hS-TpF" id="Dy3-1A-JV4"/>
</connections>
</barButtonItem>
<barButtonItem id="Ih6-jI-jFg">
<view key="customView" contentMode="scaleToFill" id="gSl-PT-7DH">
<rect key="frame" x="332" y="12" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="pdn-6v-d9a">
<rect key="frame" x="0.0" y="0.0" width="20" height="20"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="pdn-6v-d9a" firstAttribute="width" secondItem="gSl-PT-7DH" secondAttribute="width" id="Bpz-h0-KzN"/>
<constraint firstItem="pdn-6v-d9a" firstAttribute="centerY" secondItem="gSl-PT-7DH" secondAttribute="centerY" id="L9F-HX-b30"/>
<constraint firstItem="pdn-6v-d9a" firstAttribute="height" secondItem="gSl-PT-7DH" secondAttribute="height" id="O6T-RS-feo"/>
<constraint firstItem="pdn-6v-d9a" firstAttribute="centerX" secondItem="gSl-PT-7DH" secondAttribute="centerX" id="ysw-er-kKl"/>
</constraints>
</view>
</barButtonItem>
</rightBarButtonItems>
</navigationItem>
<connections>
<outlet property="actionButton" destination="7L9-X7-1Oc" id="VnP-sl-Cmd"/>
@ -845,6 +896,7 @@ Don't have a Reader account?</string>
<outlet property="apiURLTextField" destination="iPv-M2-U8Q" id="8kn-Xk-a8w"/>
<outlet property="cancelBarButtonItem" destination="n8H-ai-4Df" id="u86-HH-HYC"/>
<outlet property="footerLabel" destination="7Jj-p8-lYw" id="Tqv-qR-WBR"/>
<outlet property="onepasswordButton" destination="MLD-vW-VQN" id="y3T-hD-XRD"/>
<outlet property="passwordTextField" destination="KgN-kQ-Cyc" id="A0K-JL-CEW"/>
<outlet property="showHideButton" destination="cFF-qt-WLs" id="AxI-Gl-NdM"/>
<outlet property="signUpButton" destination="3Fq-U4-PS5" id="Wuj-5g-vDH"/>
@ -883,7 +935,7 @@ Don't have a Reader account?</string>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Use your iCloud account to sync your subscriptions across your iOS and macOS devices." textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="aFS-Y0-2MH">
<rect key="frame" x="20" y="8" width="373" height="30.5"/>
<rect key="frame" x="20" y="8" width="373" height="26.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
@ -968,6 +1020,7 @@ Don't have a Reader account?</string>
</scene>
</scenes>
<resources>
<image name="1password" width="23" height="23"/>
<namedColor name="secondaryAccentColor">
<color red="0.031372549019607843" green="0.41568627450980394" blue="0.93333333333333335" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>

View File

@ -21,6 +21,11 @@ class FeedWranglerAccountViewController: UITableViewController {
@IBOutlet weak var showHideButton: UIButton!
@IBOutlet weak var actionButton: UIButton!
@IBOutlet weak var footerLabel: UILabel!
@IBOutlet weak var onepasswordButton: UIBarButtonItem! {
didSet {
onepasswordButton.image?.withTintColor(AppAssets.primaryAccentColor)
}
}
weak var account: Account?
weak var delegate: AddAccountDismissDelegate?
@ -152,6 +157,17 @@ class FeedWranglerAccountViewController: UITableViewController {
actionButton.isEnabled = !(emailTextField.text?.isEmpty ?? false) && !(passwordTextField.text?.isEmpty ?? false)
}
@IBAction func retrievePasswordDetailsFrom1Password(_ sender: Any) {
OnePasswordExtension.shared().findLogin(forURLString: "feedwrangler.com", for: self, sender: nil) { [self] loginDictionary, error in
if let loginDictionary = loginDictionary {
emailTextField.text = loginDictionary[AppExtensionUsernameKey] as? String
passwordTextField.text = loginDictionary[AppExtensionPasswordKey] as? String
actionButton.isEnabled = !(emailTextField.text?.isEmpty ?? false) && !(passwordTextField.text?.isEmpty ?? false)
}
}
}
private func showError(_ message: String) {
presentError(title: NSLocalizedString("Error", comment: "Credentials Error"), message: message)
}

View File

@ -21,6 +21,11 @@ class FeedbinAccountViewController: UITableViewController {
@IBOutlet weak var showHideButton: UIButton!
@IBOutlet weak var actionButton: UIButton!
@IBOutlet weak var footerLabel: UILabel!
@IBOutlet weak var onepasswordButton: UIBarButtonItem! {
didSet {
onepasswordButton.image?.withTintColor(AppAssets.primaryAccentColor)
}
}
weak var account: Account?
weak var delegate: AddAccountDismissDelegate?
@ -46,6 +51,11 @@ class FeedbinAccountViewController: UITableViewController {
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: passwordTextField)
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
if !OnePasswordExtension.shared().isAppExtensionAvailable() {
onepasswordButton.isEnabled = false
onepasswordButton.image?.withTintColor(.clear)
}
}
private func setupFooter() {
@ -70,6 +80,17 @@ class FeedbinAccountViewController: UITableViewController {
dismiss(animated: true, completion: nil)
}
@IBAction func retrievePasswordDetailsFrom1Password(_ sender: Any) {
OnePasswordExtension.shared().findLogin(forURLString: "feedbin.com", for: self, sender: nil) { [self] loginDictionary, error in
if let loginDictionary = loginDictionary {
emailTextField.text = loginDictionary[AppExtensionUsernameKey] as? String
passwordTextField.text = loginDictionary[AppExtensionPasswordKey] as? String
actionButton.isEnabled = !(emailTextField.text?.isEmpty ?? false) && !(passwordTextField.text?.isEmpty ?? false)
}
}
}
@IBAction func showHidePassword(_ sender: Any) {
if passwordTextField.isSecureTextEntry {
passwordTextField.isSecureTextEntry = false

View File

@ -21,6 +21,11 @@ class NewsBlurAccountViewController: UITableViewController {
@IBOutlet weak var showHideButton: UIButton!
@IBOutlet weak var actionButton: UIButton!
@IBOutlet weak var footerLabel: UILabel!
@IBOutlet weak var onepasswordButton: UIBarButtonItem! {
didSet {
onepasswordButton.image?.withTintColor(AppAssets.primaryAccentColor)
}
}
weak var account: Account?
weak var delegate: AddAccountDismissDelegate?
@ -152,6 +157,16 @@ class NewsBlurAccountViewController: UITableViewController {
safari.modalPresentationStyle = .currentContext
self.present(safari, animated: true, completion: nil)
}
@IBAction func retrievePasswordDetailsFrom1Password(_ sender: Any) {
OnePasswordExtension.shared().findLogin(forURLString: "newsblur.com", for: self, sender: nil) { [self] loginDictionary, error in
if let loginDictionary = loginDictionary {
usernameTextField.text = loginDictionary[AppExtensionUsernameKey] as? String
passwordTextField.text = loginDictionary[AppExtensionPasswordKey] as? String
actionButton.isEnabled = !(usernameTextField.text?.isEmpty ?? false) && !(passwordTextField.text?.isEmpty ?? false)
}
}
}
@objc func textDidChange(_ note: Notification) {
actionButton.isEnabled = !(usernameTextField.text?.isEmpty ?? false)

View File

@ -23,6 +23,11 @@ class ReaderAPIAccountViewController: UITableViewController {
@IBOutlet weak var actionButton: UIButton!
@IBOutlet weak var footerLabel: UILabel!
@IBOutlet weak var signUpButton: UIButton!
@IBOutlet weak var onepasswordButton: UIBarButtonItem! {
didSet {
onepasswordButton.image?.withTintColor(AppAssets.primaryAccentColor)
}
}
weak var account: Account?
var accountType: AccountType?
@ -264,6 +269,30 @@ class ReaderAPIAccountViewController: UITableViewController {
self.present(safari, animated: true, completion: nil)
}
@IBAction func retrievePasswordDetailsFrom1Password(_ sender: Any) {
var url: String
switch accountType {
case .bazQux:
url = "bazqux.com"
case .inoreader:
url = "inoreader.com"
case .theOldReader:
url = "theoldreader.com"
case .freshRSS:
url = apiURLTextField.text ?? ""
default:
url = ""
}
OnePasswordExtension.shared().findLogin(forURLString: url, for: self, sender: nil) { [self] loginDictionary, error in
if let loginDictionary = loginDictionary {
usernameTextField.text = loginDictionary[AppExtensionUsernameKey] as? String
passwordTextField.text = loginDictionary[AppExtensionPasswordKey] as? String
actionButton.isEnabled = !(usernameTextField.text?.isEmpty ?? false) && !(passwordTextField.text?.isEmpty ?? false)
}
}
}
private func apiURL() -> URL? {
switch accountType {
case .freshRSS:

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "onepassword-navbar.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "onepassword-navbar@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "onepassword-navbar@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -57,6 +57,7 @@
<key>LSApplicationQueriesSchemes</key>
<array>
<string>mailto</string>
<string>org-appextension-feature-password-management</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>

View File

@ -0,0 +1,5 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import "OnePasswordExtension.h"

View File

@ -41,3 +41,7 @@ INFOPLIST_FILE = iOS/Resources/Info.plist
CODE_SIGN_ENTITLEMENTS = iOS/Resources/NetNewsWire$(DEVELOPER_ENTITLEMENTS).entitlements
PRODUCT_BUNDLE_IDENTIFIER = $(ORGANIZATION_IDENTIFIER).NetNewsWire.iOS
PRODUCT_NAME = NetNewsWire
CLANG_ENABLE_MODULES = YES
LD_RUNPATH_SEARCH_PATHS = ("$(inherited)","@executable_path/Frameworks",)
SWIFT_OBJC_BRIDGING_HEADER = iOS/Resources/NetNewsWire-iOS-Bridging-Header.h
SWIFT_VERSION = 5.0