From 4fad1626bc7cdefdf31459ef8a171e18a1a27010 Mon Sep 17 00:00:00 2001 From: David Sinclair Date: Fri, 27 Mar 2020 20:55:05 -0700 Subject: [PATCH] #1270 (replacing UIWebView with WKWebView) - Updated 1Password extension. --- .../OnePasswordExtension/LICENSE.txt | 2 +- .../OnePasswordExtension.h | 267 +++++++------- .../OnePasswordExtension.m | 338 ++++++++---------- .../OnePasswordExtension/README.md | 136 ++++--- 4 files changed, 387 insertions(+), 356 deletions(-) diff --git a/clients/ios/Other Sources/OnePasswordExtension/LICENSE.txt b/clients/ios/Other Sources/OnePasswordExtension/LICENSE.txt index fbe2df3c89..a2ee23cd0a 100644 --- a/clients/ios/Other Sources/OnePasswordExtension/LICENSE.txt +++ b/clients/ios/Other Sources/OnePasswordExtension/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2014 AgileBits Inc. +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 diff --git a/clients/ios/Other Sources/OnePasswordExtension/OnePasswordExtension.h b/clients/ios/Other Sources/OnePasswordExtension/OnePasswordExtension.h index 6213dde7ad..db7256c030 100644 --- a/clients/ios/Other Sources/OnePasswordExtension/OnePasswordExtension.h +++ b/clients/ios/Other Sources/OnePasswordExtension/OnePasswordExtension.h @@ -1,9 +1,22 @@ +//Copyright (c) 2014-2020 AgileBits Inc. // -// 1Password Extension +//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: // -// Lovingly handcrafted by Dave Teare, Michael Fey, Rad Azzouz, and Roustem Karimov. -// Copyright (c) 2014 AgileBits. All rights reserved. +//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 #import @@ -13,51 +26,70 @@ #import #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 -#define AppExtensionURLStringKey @"url_string" -#define AppExtensionUsernameKey @"username" -#define AppExtensionPasswordKey @"password" -#define AppExtensionTOTPKey @"totp" -#define AppExtensionTitleKey @"login_title" -#define AppExtensionNotesKey @"notes" -#define AppExtensionSectionTitleKey @"section_title" -#define AppExtensionFieldsKey @"fields" -#define AppExtensionReturnedFieldsKey @"returned_fields" -#define AppExtensionOldPasswordKey @"old_password" -#define AppExtensionPasswordGeneratorOptionsKey @"password_generator_options" + +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 -#define AppExtensionGeneratedPasswordMinLengthKey @"password_min_length" -#define AppExtensionGeneratedPasswordMaxLengthKey @"password_max_length" +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 -#define AppExtensionErrorDomain @"OnePasswordExtension" - -#define AppExtensionErrorCodeCancelledByUser 0 -#define AppExtensionErrorCodeAPINotAvailable 1 -#define AppExtensionErrorCodeFailedToContactExtension 2 -#define AppExtensionErrorCodeFailedToLoadItemProviderData 3 -#define AppExtensionErrorCodeCollectFieldsScriptFailed 4 -#define AppExtensionErrorCodeFillFieldsScriptFailed 5 -#define AppExtensionErrorCodeUnexpectedData 6 -#define AppExtensionErrorCodeFailedToObtainURLStringFromWebView 7 +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. -// You might to so by adding your own project prefix, e.g., MyLibraryOnePasswordExtension. +// 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; /*! - Determines if the 1Password Extension is available. Allows you to only show the 1Password login button to those + @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 :) - - Note that this returns YES if any app that supports the generic `org-appextension-feature-password-management` feature - is installed. + + @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."); @@ -67,129 +99,122 @@ /*! 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 database 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 database. This is especially useful when the user has a login for "https://olddomain.com". - + + @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 the URLString for matching Logins in the 1Password database. - - @param the view controller from which the 1Password Extension is invoked. Usually `self` - - @param the sender which triggers the share sheet to show. UIButton, UIBarButtonItem or UIView. Can also be nil on the iPhone, but not on the iPad. - - @param Login Dictionary Reply parameter that contains the username, password and the One-Time Password if available. - - @param error Reply parameter that is nil if the 1Password Extension has been successfully completed, or it contains error information about the completion failure. + + @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:(NSString *)URLString forViewController:(UIViewController *)viewController sender:(id)sender completion:(void (^)(NSDictionary *loginDictionary, NSError *error))completion; +- (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 the URLString for the Login to be saved in 1Password. - - @param details about the Login to be saved, including custom fields, are stored in an dictionary and given to the 1Password Extension. - - @param the Password Generator Options represented in a dictionary form. - - @param the view controller from which the 1Password Extension is invoked. Usually `self` - - @param the sender which triggers the share sheet to show. UIButton, UIBarButtonItem or UIView. Can also be nil on the iPhone, but not on the iPad. - - @param Login dictionary 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. - - @param error Reply parameter that is nil if the 1Password Extension has been successfully completed, or it contains error information about the completion failure. + + @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:(NSString *)URLString loginDetails:(NSDictionary *)loginDetailsDictionary passwordGenerationOptions:(NSDictionary *)passwordGenerationOptions forViewController:(UIViewController *)viewController sender:(id)sender completion:(void (^)(NSDictionary *loginDictionary, NSError *error))completion; +- (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 signle matching Login is found: 1Password will enter edit mode for that Login and will update its password using the value for AppExtensionPasswordKey. + 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 the URLString for the Login to be updated with a new password in 1Password. - - @param the details about the Login to be saved, including old password and the username, are stored in an dictionary and given to the 1Password Extension. - - @param the Password Generator Options represented in a dictionary form. - - @param the view controller from which the 1Password Extension is invoked. Usually `self` - - @param the sender which triggers the share sheet to show. UIButton, UIBarButtonItem or UIView. Can also be nil on the iPhone, but not on the iPad. - - @param Login dictionary 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. - - @param error Reply parameter that is nil if the 1Password Extension has been successfully completed, or it contains error information about the completion failure. + + @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:(NSString *)URLString loginDetails:(NSDictionary *)loginDetailsDict passwordGenerationOptions:(NSDictionary *)passwordGenerationOptions forViewController:(UIViewController *)viewController sender:(id)sender completion:(void (^)(NSDictionary *loginDictionary, NSError *error))completion; +- (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 both WKWebView and UIWebView. - - @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 database 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. - + 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 the URLString for matching Logins in the 1Password database. - - @param the view controller from which the 1Password Extension is invoked. Usually `self`. - - @param the sender which triggers the share sheet to show. UIButton, UIBarButtonItem or UIView. Can also be nil on the iPhone, but not on the iPad. - - @param success Reply parameter that is YES if the 1Password Extension has been successfully completed or NO otherwise. - - @param error Reply parameter that is nil if the 1Password Extension has been successfully completed, or it contains error information about the completion failure. + + @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:(id)webView forViewController:(UIViewController *)viewController sender:(id)sender showOnlyLogins:(BOOL)yesOrNo completion:(void (^)(BOOL success, NSError *error))completion; +- (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 the bundle identidier of the selected activity in the share sheet. - @return YES if the selected activity is the 1Password extension, NO otherwise. + + @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:(NSString *)activityType; +- (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 the active UIWebView Or WKWebView. Must not be nil. - - @param extension item Reply parameter that is contains all the info required by the 1Password extension if has been successfully completed or nil otherwise. - - @param error Reply parameter that is nil if the 1Password extension item has been successfully created, or it contains error information about the completion failure. + + @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:(id)webView completion:(void (^)(NSExtensionItem *extensionItem, NSError *error))completion; +- (void)createExtensionItemForWebView:(nonnull WKWebView *)webView completion:(nonnull OnePasswordExtensionItemCompletionBlock)completion; /*! Method used in the UIActivityViewController completion block to fill information into a web view. - - @param array that contains the selected activity in the share sheet. Empty array if the share sheet is cancelled by the user. - @param the active UIWebView Or WKWebView. Must not be nil. - @param success Reply parameter that is YES if the 1Password Extension has been successfully completed or NO otherwise. - - @param 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:(NSArray *)returnedItems intoWebView:(id)webView completion:(void (^)(BOOL success, NSError *error))completion; - -/*! - Deprecated in version 1.3. - @see Use fillItemIntoWebView:forViewController:sender:showOnlyLogins:completion: instead + @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)fillLoginIntoWebView:(id)webView forViewController:(UIViewController *)viewController sender:(id)sender completion:(void (^)(BOOL success, NSError *error))completion __attribute__((deprecated("Use fillItemIntoWebView:forViewController:sender:showOnlyLogins:completion: instead. Deprecated in version 1.3"))); +- (void)fillReturnedItems:(nullable NSArray *)returnedItems intoWebView:(nonnull WKWebView *)webView completion:(nonnull OnePasswordSuccessCompletionBlock)completion; @end + +#if __has_feature(nullability) +NS_ASSUME_NONNULL_END +#endif diff --git a/clients/ios/Other Sources/OnePasswordExtension/OnePasswordExtension.m b/clients/ios/Other Sources/OnePasswordExtension/OnePasswordExtension.m index 28707b415a..b19667bc52 100644 --- a/clients/ios/Other Sources/OnePasswordExtension/OnePasswordExtension.m +++ b/clients/ios/Other Sources/OnePasswordExtension/OnePasswordExtension.m @@ -1,14 +1,47 @@ +//Copyright (c) 2014-2020 AgileBits Inc. // -// 1Password Extension +//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: // -// Lovingly handcrafted by Dave Teare, Michael Fey, Rad Azzouz, and Roustem Karimov. -// Copyright (c) 2014 AgileBits. All rights reserved. +//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 @(120) +#define VERSION_NUMBER @(185) static NSString *const AppExtensionVersionNumberKey = @"version_number"; // Available App Extension Actions @@ -37,14 +70,6 @@ + (OnePasswordExtension *)sharedExtension { return __sharedExtension; } -- (BOOL)isSystemAppExtensionAPIAvailable { -#ifdef __IPHONE_8_0 - return NSClassFromString(@"NSExtensionItem") != nil; -#else - return NO; -#endif -} - - (BOOL)isAppExtensionAvailable { if ([self isSystemAppExtensionAPIAvailable]) { return [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"org-appextension-feature-password-management://"]]; @@ -55,7 +80,7 @@ - (BOOL)isAppExtensionAvailable { #pragma mark - Native app Login -- (void)findLoginForURLString:(NSString *)URLString forViewController:(UIViewController *)viewController sender:(id)sender completion:(void (^)(NSDictionary *loginDictionary, NSError *error))completion { +- (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"); @@ -68,7 +93,6 @@ - (void)findLoginForURLString:(NSString *)URLString forViewController:(UIViewCon return; } -#ifdef __IPHONE_8_0 NSDictionary *item = @{ AppExtensionVersionNumberKey: VERSION_NUMBER, AppExtensionURLStringKey: URLString }; UIActivityViewController *activityViewController = [self activityViewControllerForItem:item viewController:viewController sender:sender typeIdentifier:kUTTypeAppExtensionFindLoginAction]; @@ -98,14 +122,12 @@ - (void)findLoginForURLString:(NSString *)URLString forViewController:(UIViewCon }; [viewController presentViewController:activityViewController animated:YES completion:nil]; -#endif } #pragma mark - New User Registration -- (void)storeLoginForURLString:(NSString *)URLString loginDetails:(NSDictionary *)loginDetailsDictionary passwordGenerationOptions:(NSDictionary *)passwordGenerationOptions forViewController:(UIViewController *)viewController sender:(id)sender completion:(void (^)(NSDictionary *loginDictionary, NSError *error))completion { +- (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(loginDetailsDictionary != nil, @"loginDetailsDict must not be nil"); NSAssert(viewController != nil, @"viewController must not be nil"); if (NO == [self isSystemAppExtensionAPIAvailable]) { @@ -117,8 +139,6 @@ - (void)storeLoginForURLString:(NSString *)URLString loginDetails:(NSDictionary return; } - -#ifdef __IPHONE_8_0 NSMutableDictionary *newLoginAttributesDict = [NSMutableDictionary new]; newLoginAttributesDict[AppExtensionVersionNumberKey] = VERSION_NUMBER; newLoginAttributesDict[AppExtensionURLStringKey] = URLString; @@ -154,12 +174,11 @@ - (void)storeLoginForURLString:(NSString *)URLString loginDetails:(NSDictionary }; [viewController presentViewController:activityViewController animated:YES completion:nil]; -#endif } #pragma mark - Change Password -- (void)changePasswordForLoginForURLString:(NSString *)URLString loginDetails:(NSDictionary *)loginDetailsDict passwordGenerationOptions:(NSDictionary *)passwordGenerationOptions forViewController:(UIViewController *)viewController sender:(id)sender completion:(void (^)(NSDictionary *loginDictionary, NSError *error))completion { +- (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"); @@ -172,11 +191,10 @@ - (void)changePasswordForLoginForURLString:(NSString *)URLString loginDetails:(N return; } -#ifdef __IPHONE_8_0 NSMutableDictionary *item = [NSMutableDictionary new]; item[AppExtensionVersionNumberKey] = VERSION_NUMBER; item[AppExtensionURLStringKey] = URLString; - [item addEntriesFromDictionary:loginDetailsDict]; + [item addEntriesFromDictionary:loginDetailsDictionary]; if (passwordGenerationOptions.count > 0) { item[AppExtensionPasswordGeneratorOptionsKey] = passwordGenerationOptions; } @@ -209,86 +227,57 @@ - (void)changePasswordForLoginForURLString:(NSString *)URLString loginDetails:(N }; [viewController presentViewController:activityViewController animated:YES completion:nil]; -#endif } #pragma mark - Web View filling Support -- (void)fillItemIntoWebView:(id)webView forViewController:(UIViewController *)viewController sender:(id)sender showOnlyLogins:(BOOL)yesOrNo completion:(void (^)(BOOL success, NSError *error))completion { +- (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."); -#ifdef __IPHONE_8_0 - if ([webView isKindOfClass:[UIWebView class]]) { - [self fillItemIntoUIWebView:webView webViewController:viewController sender:(id)sender showOnlyLogins:yesOrNo completion:^(BOOL success, NSError *error) { - if (completion) { - completion(success, error); - } - }]; - } -#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0 || ONE_PASSWORD_EXTENSION_ENABLE_WK_WEB_VIEW - else if ([webView isKindOfClass:[WKWebView class]]) { - [self fillItemIntoWKWebView:webView forViewController:viewController sender:(id)sender showOnlyLogins:yesOrNo completion:^(BOOL success, NSError *error) { - if (completion) { - completion(success, error); - } - }]; - } -#endif - else { - [NSException raise:@"Invalid argument: web view must be an instance of WKWebView or UIWebView." format:@""]; - } -#endif + [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:(NSString *)activityType { +- (BOOL)isOnePasswordExtensionActivityType:(nullable NSString *)activityType { return [@"com.agilebits.onepassword-ios.extension" isEqualToString:activityType] || [@"com.agilebits.beta.onepassword-ios.extension" isEqualToString:activityType]; } -- (void)createExtensionItemForWebView:(id)webView completion:(void (^)(NSExtensionItem *extensionItem, NSError *error))completion { +- (void)createExtensionItemForWebView:(nonnull WKWebView *)webView completion:(nonnull OnePasswordExtensionItemCompletionBlock)completion { NSAssert(webView != nil, @"webView must not be nil"); - -#ifdef __IPHONE_8_0 - if ([webView isKindOfClass:[UIWebView class]]) { - UIWebView *uiWebView = (UIWebView *)webView; - NSString *collectedPageDetails = [uiWebView stringByEvaluatingJavaScriptFromString:OPWebViewCollectFieldsScript]; - - [self createExtensionItemForURLString:uiWebView.request.URL.absoluteString webPageDetails:collectedPageDetails completion:completion]; - } -#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0 || ONE_PASSWORD_EXTENSION_ENABLE_WK_WEB_VIEW - else if ([webView isKindOfClass:[WKWebView class]]) { - WKWebView *wkWebView = (WKWebView *)webView; - [wkWebView 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]) { + 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); - } - else { - dispatch_async(dispatch_get_main_queue(), ^{ - completion(nil, failedToCollectFieldsError); - }); - } + }); } - - return; } - [self createExtensionItemForURLString:wkWebView.URL.absoluteString webPageDetails:result completion:completion]; - }]; - } -#endif - else { - [NSException raise:@"Invalid argument: web view must be an instance of WKWebView or UIWebView." format:@""]; - } -#endif + return; + } + + [self createExtensionItemForURLString:webView.URL.absoluteString webPageDetails:result completion:completion]; + }]; } -- (void)fillReturnedItems:(NSArray *)returnedItems intoWebView:(id)webView completion:(void (^)(BOOL success, NSError *error))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) { @@ -318,7 +307,11 @@ - (void)fillReturnedItems:(NSArray *)returnedItems intoWebView:(id)webView compl #pragma mark - Private methods -- (void)findLoginIn1PasswordWithURLString:(NSString *)URLString collectedPageDetails:(NSString *)collectedPageDetails forWebViewController:(UIViewController *)forViewController sender:(id)sender withWebView:(id)webView showOnlyLogins:(BOOL)yesOrNo completion:(void (^)(BOOL success, NSError *error))completion { +- (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); @@ -383,8 +376,7 @@ - (void)findLoginIn1PasswordWithURLString:(NSString *)URLString collectedPageDet [forViewController presentViewController:activityViewController animated:YES completion:nil]; } -#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0 || ONE_PASSWORD_EXTENSION_ENABLE_WK_WEB_VIEW -- (void)fillItemIntoWKWebView:(WKWebView *)webView forViewController:(UIViewController *)viewController sender:(id)sender showOnlyLogins:(BOOL)yesOrNo completion:(void (^)(BOOL success, NSError *error))completion { +- (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); @@ -402,72 +394,37 @@ - (void)fillItemIntoWKWebView:(WKWebView *)webView forViewController:(UIViewCont }]; }]; } -#endif -- (void)fillItemIntoUIWebView:(UIWebView *)webView webViewController:(UIViewController *)viewController sender:(id)sender showOnlyLogins:(BOOL)yesOrNo completion:(void (^)(BOOL success, NSError *error))completion { - NSString *collectedPageDetails = [webView stringByEvaluatingJavaScriptFromString:OPWebViewCollectFieldsScript]; - [self findLoginIn1PasswordWithURLString:webView.request.URL.absoluteString collectedPageDetails:collectedPageDetails forWebViewController:viewController sender:sender withWebView:webView showOnlyLogins:yesOrNo completion:^(BOOL success, NSError *error) { - if (completion) { - completion(success, error); - } - }]; -} +- (void)executeFillScript:(NSString * __nullable)fillScript inWebView:(nonnull WKWebView *)webView completion:(nonnull OnePasswordSuccessCompletionBlock)completion { -- (void)executeFillScript:(NSString *)fillScript inWebView:(id)webView completion:(void (^)(BOOL success, NSError *error))completion { if (fillScript == nil) { NSLog(@"Failed to executeFillScript, fillScript is missing"); if (completion) { - completion(NO, [OnePasswordExtension failedToFillFieldsErrorWithLocalizedErrorMessage:NSLocalizedString(@"Failed to fill web page because script is missing", @"1Password Extension Error Message") underlyingError:nil]); + 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, %@);", fillScript]; + [scriptSource appendFormat:@"(document, %@, undefined);", fillScript]; - if ([webView isKindOfClass:[UIWebView class]]) { - NSString *result = [((UIWebView *)webView) stringByEvaluatingJavaScriptFromString:scriptSource]; + [webView evaluateJavaScript:scriptSource completionHandler:^(NSString *result, NSError *evaluationError) { BOOL success = (result != nil); NSError *error = nil; if (!success) { - NSLog(@"Cannot executeFillScript, stringByEvaluatingJavaScriptFromString failed"); - error = [OnePasswordExtension failedToFillFieldsErrorWithLocalizedErrorMessage:NSLocalizedString(@"Failed to fill web page because script could not be evaluated", @"1Password Extension Error Message") underlyingError:nil]; + 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); } - - return; - } - -#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0 || ONE_PASSWORD_EXTENSION_ENABLE_WK_WEB_VIEW - if ([webView isKindOfClass:[WKWebView class]]) { - [((WKWebView *)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:NSLocalizedString(@"Failed to fill web page because script could not be evaluated", @"1Password Extension Error Message") underlyingError:error]; - } - - if (completion) { - completion(success, error); - } - }]; - - return; - } -#endif - - [NSException raise:@"Invalid argument: web view must be an instance of WKWebView or UIWebView." format:@""]; + }]; } -#ifdef __IPHONE_8_0 -- (void)processExtensionItem:(NSExtensionItem *)extensionItem completion:(void (^)(NSDictionary *itemDictionary, NSError *error))completion { +- (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]; @@ -478,7 +435,7 @@ - (void)processExtensionItem:(NSExtensionItem *)extensionItem completion:(void ( } NSItemProvider *itemProvider = extensionItem.attachments.firstObject; - if (NO == [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypePropertyList]) { + 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) { @@ -488,7 +445,7 @@ - (void)processExtensionItem:(NSExtensionItem *)extensionItem completion:(void ( } - [itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypePropertyList options:nil completionHandler:^(NSDictionary *itemDictionary, NSError *itemProviderError) { + [itemProvider loadItemForTypeIdentifier:(__bridge NSString *)kUTTypePropertyList options:nil completionHandler:^(NSDictionary *itemDictionary, NSError *itemProviderError) { NSError *error = nil; if (itemDictionary.count == 0) { NSLog(@"Failed to loadItemForTypeIdentifier: %@", itemProviderError); @@ -508,8 +465,8 @@ - (void)processExtensionItem:(NSExtensionItem *)extensionItem completion:(void ( }]; } -- (UIActivityViewController *)activityViewControllerForItem:(NSDictionary *)item viewController:(UIViewController*)viewController sender:(id)sender typeIdentifier:(NSString *)typeIdentifier { -#ifdef __IPHONE_8_0 +- (UIActivityViewController *)activityViewControllerForItem:(nonnull NSDictionary *)item viewController:(nonnull UIViewController*)viewController sender:(nullable id)sender typeIdentifier:(nonnull NSString *)typeIdentifier { + NSAssert(NO == (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad && sender == nil), @"sender must not be nil on iPad."); NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithItem:item typeIdentifier:typeIdentifier]; @@ -530,14 +487,9 @@ - (UIActivityViewController *)activityViewControllerForItem:(NSDictionary *)item } return controller; -#else - return nil; -#endif } -#endif - -- (void)createExtensionItemForURLString:(NSString *)URLString webPageDetails:(NSString *)webPageDetails completion:(void (^)(NSExtensionItem *extensionItem, NSError *error))completion { +- (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]; @@ -572,19 +524,19 @@ - (void)createExtensionItemForURLString:(NSString *)URLString webPageDetails:(NS #pragma mark - Errors + (NSError *)systemAppExtensionAPINotAvailableError { - NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : NSLocalizedString(@"App Extension API is not available is this version of iOS", @"1Password Extension Error Message") }; + 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 : NSLocalizedString(@"1Password Extension was cancelled by the user", @"1Password Extension Error Message") }; + 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:(NSError *)activityError { ++ (NSError *)failedToContactExtensionErrorWithActivityError:(nullable NSError *)activityError { NSMutableDictionary *userInfo = [NSMutableDictionary new]; - userInfo[NSLocalizedDescriptionKey] = NSLocalizedString(@"Failed to contact the 1Password Extension", @"1Password Extension Error Message"); + userInfo[NSLocalizedDescriptionKey] = NSLocalizedStringFromTable(@"Failed to contact the 1Password Extension", @"OnePasswordExtension", @"1Password Extension Error Message"); if (activityError) { userInfo[NSUnderlyingErrorKey] = activityError; } @@ -592,9 +544,9 @@ + (NSError *)failedToContactExtensionErrorWithActivityError:(NSError *)activityE return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeFailedToContactExtension userInfo:userInfo]; } -+ (NSError *)failedToCollectFieldsErrorWithUnderlyingError:(NSError *)underlyingError { ++ (NSError *)failedToCollectFieldsErrorWithUnderlyingError:(nullable NSError *)underlyingError { NSMutableDictionary *userInfo = [NSMutableDictionary new]; - userInfo[NSLocalizedDescriptionKey] = NSLocalizedString(@"Failed to execute script that collects web page information", @"1Password Extension Error Message"); + userInfo[NSLocalizedDescriptionKey] = NSLocalizedStringFromTable(@"Failed to execute script that collects web page information", @"OnePasswordExtension", @"1Password Extension Error Message"); if (underlyingError) { userInfo[NSUnderlyingErrorKey] = underlyingError; } @@ -602,7 +554,7 @@ + (NSError *)failedToCollectFieldsErrorWithUnderlyingError:(NSError *)underlying return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeCollectFieldsScriptFailed userInfo:userInfo]; } -+ (NSError *)failedToFillFieldsErrorWithLocalizedErrorMessage:(NSString *)errorMessage underlyingError:(NSError *)underlyingError { ++ (NSError *)failedToFillFieldsErrorWithLocalizedErrorMessage:(nullable NSString *)errorMessage underlyingError:(nullable NSError *)underlyingError { NSMutableDictionary *userInfo = [NSMutableDictionary new]; if (errorMessage) { userInfo[NSLocalizedDescriptionKey] = errorMessage; @@ -614,9 +566,9 @@ + (NSError *)failedToFillFieldsErrorWithLocalizedErrorMessage:(NSString *)errorM return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeFillFieldsScriptFailed userInfo:userInfo]; } -+ (NSError *)failedToLoadItemProviderDataErrorWithUnderlyingError:(NSError *)underlyingError { ++ (NSError *)failedToLoadItemProviderDataErrorWithUnderlyingError:(nullable NSError *)underlyingError { NSMutableDictionary *userInfo = [NSMutableDictionary new]; - userInfo[NSLocalizedDescriptionKey] = NSLocalizedString(@"Failed to parse information returned by 1Password Extension", @"1Password Extension Error Message"); + userInfo[NSLocalizedDescriptionKey] = NSLocalizedStringFromTable(@"Failed to parse information returned by 1Password Extension", @"OnePasswordExtension", @"1Password Extension Error Message"); if (underlyingError) { userInfo[NSUnderlyingErrorKey] = underlyingError; } @@ -625,50 +577,62 @@ + (NSError *)failedToLoadItemProviderDataErrorWithUnderlyingError:(NSError *)und } + (NSError *)failedToObtainURLStringFromWebViewError { - NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : NSLocalizedString(@"Failed to obtain URL String from web view. The web view must be loaded completely when calling the 1Password Extension", @"1Password Extension Error Message") }; + 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.com_agilebits_onepassword_collect=n;document.elementsByOPID={};\ -function n(d,b){function e(a,f){var c=a[f];if('string'==typeof c)return c;c=a.getAttribute(f);return'string'==typeof c?c:null}function h(a){switch(l(a.type)){case 'checkbox':return a.checked?'✓':'';case 'hidden':a=a.value;if(!a||'number'!=typeof a.length)return'';254\\/?]/mg,\ -''):null;return[c?c:null,a.value]}),{options:a}):null}function s(a){var f;for(a=a.parentElement||a.parentNode;a&&'td'!=l(a.tagName);)a=a.parentElement||a.parentNode;if(!a||void 0===a)return null;f=a.parentElement||a.parentNode;if('tr'!=f.tagName.toLowerCase())return null;f=f.previousElementSibling;if(!f||'tr'!=(f.tagName+'').toLowerCase()||f.cells&&a.cellIndex>=f.cells.length)return null;a=p(f.cells[a.cellIndex]);return a=r(a)}function x(a){var f=d.documentElement,c=a.getBoundingClientRect(),b=f.getBoundingClientRect(),\ -e=c.left-f.clientLeft,f=c.top-f.clientTop;return a.offsetParent?0>e||e>b.width||0>f||f>b.height?t(a):(b=a.ownerDocument.elementFromPoint(e+3,f+3))?'label'===l(b.tagName)?b===y(a):b.tagName===a.tagName:!1:!1}function t(a){for(var f;a!==d&&a;a=a.parentNode)if(f=u.getComputedStyle?u.getComputedStyle(a,null):a.style,'none'===f.display||'hidden'==f.visibility)return!1;return a===d}function y(a){var f;if(a.id&&(f=z(d,'label[for='+JSON.stringify(a.id)+']'))||a.name&&(f=z(d,'label[for='+JSON.stringify(a.name)+\ -']')))return p(f);for(;a&&a!=d;a=a.parentNode)if('label'===l(a.tagName))return p(a);return null}function g(a,f,c,d){void 0!==d&&d===c||null===c||void 0===c||(a[f]=c)}function l(a){return'string'===typeof a?a.toLowerCase():(''+a).toLowerCase()}function z(a,d){var c=null;try{c=a.querySelector(d)}catch(b){}return c}function A(a,d){var c=[];try{c=a.querySelectorAll(d)}catch(b){}return c}var u=d.defaultView?d.defaultView:window,m=RegExp('((\\\\b|_|-)pin(\\\\b|_|-)|password|passwort|kennwort|passe|contraseña|senha|密码|adgangskode|hasło|wachtwoord)',\ -'i'),F=Array.prototype.slice.call(A(d,'form')).map(function(a,d){var c={},b='__form__'+d;a.opid=b;c.opid=b;g(c,'htmlName',e(a,'name'));g(c,'htmlID',e(a,'id'));g(c,'htmlAction',v(e(a,'action')));g(c,'htmlMethod',e(a,'method'));return c}),C=Array.prototype.slice.call(A(d,'input, select')).map(function(a,f){var c={},b='__'+f,k=-1==a.maxLength?999:a.maxLength;d.elementsByOPID[b]=a;a.opid=b;c.opid=b;c.elementNumber=f;g(c,'maxLength',Math.min(k,999),999);c.visible=t(a);c.viewable=x(a);g(c,'htmlID',e(a,\ -'id'));g(c,'htmlName',e(a,'name'));g(c,'htmlClass',e(a,'class'));if('hidden'!=l(a.type)){g(c,'label-tag',y(a));g(c,'label-data',e(a,'data-label'));g(c,'label-aria',e(a,'aria-label'));g(c,'label-top',s(a));b=[];for(k=a;k&&k.nextSibling;){k=k.nextSibling;if(w(k))break;B(b,k)}g(c,'label-right',b.join(''));b=[];D(a,b);b=b.reverse().join('');g(c,'label-left',b);g(c,'placeholder',e(a,'placeholder'))}g(c,'rel',e(a,'rel'));g(c,'type',l(e(a,'type')));g(c,'value',h(a));g(c,'checked',a.checked,!1);g(c,'autoCompleteType',\ -a.getAttribute('x-autocompletetype')||a.getAttribute('autocompletetype')||a.getAttribute('autocomplete'),'off');g(c,'selectInfo',q(a));g(c,'aria-hidden','true'==a.getAttribute('aria-hidden'),!1);g(c,'aria-disabled','true'==a.getAttribute('aria-disabled'),!1);g(c,'aria-haspopup','true'==a.getAttribute('aria-haspopup'),!1);g(c,'data-stripe',e(a,'data-stripe'));a.form&&(c.form=e(a.form,'opid'));b=(m.test(c.value)||m.test(c.htmlID)||m.test(c.htmlName)||m.test(c.placeholder)||m.test(c['label-tag'])||m.test(c['label-data'])||\ -m.test(c['label-aria']))&&('text'==c.type||'password'==c.type&&!c.visible);g(c,'fakeTested',b,!1);return c});C.filter(function(a){return a.fakeTested}).forEach(function(a){var b=d.elementsByOPID[a.opid];b.getBoundingClientRect();!b||b&&'function'!==typeof b.click||b.click();b.focus();E(b,'keydown');E(b,'keyup');E(b,'keypress');b.click&&b.click();a.postFakeTestVisible=t(b);a.postFakeTestViewable=x(b);a=b.ownerDocument.createEvent('HTMLEvents');var c=b.ownerDocument.createEvent('HTMLEvents');E(b,'keydown');\ -E(b,'keyup');E(b,'keypress');c.initEvent('input',!0,!0);b.dispatchEvent(c);a.initEvent('change',!0,!0);b.dispatchEvent(a);b.blur()});return{documentUUID:b,title:d.title,url:u.location.href,forms:function(a){var b={};a.forEach(function(a){b[a.opid]=a});return b}(F),fields:C,collectedTimestamp:(new Date).getTime()}};document.elementForOPID=G;function E(d,b){var e;e=d.ownerDocument.createEvent('KeyboardEvent');e.initKeyboardEvent?e.initKeyboardEvent(b,!0,!0):e.initKeyEvent&&e.initKeyEvent(b,!0,!0,null,!1,!1,!1,!1,0,0);d.dispatchEvent(e)}function p(d){return d.textContent||d.innerText}function r(d){var b=null;d&&(b=d.replace(/^\\s+|\\s+$|\\r?\\n.*$/mg,''),b=0=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'';254b.clientWidth||10>b.clientHeight)return!1;var n=b.getClientRects();if(0===n.length)return!1;for(var p=0;pf||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 + ## Just Give Me the Code (TL;DR) -You might be looking at this 18 KB README and think integrating with 1Password is very complicated. Nothing could be further from the truth! +You might be looking at this 23 KB README and think integrating with 1Password is very complicated. Nothing could be further from the truth! If you're the type that just wants the code, here it is: -* [OnePasswordExtension.h](https://raw.githubusercontent.com/AgileBits/onepassword-app-extension/master/OnePasswordExtension.h?token=110676__eyJzY29wZSI6IlJhd0Jsb2I6QWdpbGVCaXRzL29uZXBhc3N3b3JkLWFwcC1leHRlbnNpb24vbWFzdGVyL09uZVBhc3N3b3JkRXh0ZW5zaW9uLmgiLCJleHBpcmVzIjoxNDA3Mjg0MTMwfQ%3D%3D--3867c64b22a5923bead5948001ce2ff048892799) -* [OnePasswordExtension.m](https://raw.githubusercontent.com/AgileBits/onepassword-app-extension/master/OnePasswordExtension.m?token=110676__eyJzY29wZSI6IlJhd0Jsb2I6QWdpbGVCaXRzL29uZXBhc3N3b3JkLWFwcC1leHRlbnNpb24vbWFzdGVyL09uZVBhc3N3b3JkRXh0ZW5zaW9uLm0iLCJleHBpcmVzIjoxNDA3Mjg0MTA5fQ%3D%3D--05c6ea9c73d0afb9f30e53a31d81df00b7c02077) +* [OnePasswordExtension.h](https://raw.githubusercontent.com/AgileBits/onepassword-app-extension/master/OnePasswordExtension.h) +* [OnePasswordExtension.m](https://raw.githubusercontent.com/AgileBits/onepassword-app-extension/master/OnePasswordExtension.m) Simply include these two files in your project, add a button with a [1Password login image](https://github.com/AgileBits/onepassword-app-extension/tree/master/1Password.xcassets) on it to your view, set the button's action to call the appropriate `OnePasswordExtension` method, and you're all set! @@ -32,32 +36,32 @@ Adding 1Password support to your app is easy. To demonstrate how it works, we ha ### Step 1: Download the Source Code and Sample Apps -To get started, download the 1Password App Extension API project from https://github.com/AgileBits/onepassword-app-extension/archive/master.zip, or [clone it from GitHub](https://github.com/AgileBits/onepassword-app-extension). +To get started, download the [zip version](https://github.com/AgileBits/onepassword-app-extension/archive/master.zip) of the1Password App Extension API project or [clone it from GitHub](https://github.com/AgileBits/onepassword-app-extension). Inside the downloaded folder, you'll find the resources needed to integrate with 1Password, such as images and sample code. The sample code includes two apps from ACME Corporation: one that demonstrates how to integrate the 1Password Login and Registration features, as well as a web browser that showcases the web view Filling feature. -The 1Password App Extension API is also available via CocoaPods, simply add `pod '1PasswordExtension', '~> 1.2'` (for the latest stable release) or `pod '1PasswordExtension', :git => 'https://github.com/AgileBits/onepassword-app-extension.git', :branch => 'master'` (for the latest nightly) to your Podfile, run `pod install` from your project directory and you're ready to go. +The 1Password App Extension API is also available via CocoaPods, simply add `pod '1PasswordExtension', '~> 1.8.5'` (for the latest stable release) or `pod '1PasswordExtension', :git => 'https://github.com/AgileBits/onepassword-app-extension.git', :branch => 'master'` (for the latest nightly) to your Podfile, run `pod install` from your project directory and you're ready to go. -The 1Password App Extension API is available via Carthage as well. Simply add `github AgileBits/onepassword-extension "add-framework-support"` to your Cartfile, then run `carthage boostrap` and add it to your project. +The 1Password App Extension API is available via Carthage as well. Simply add `github "AgileBits/onepassword-extension" "add-framework-support"` to your Cartfile, then run `carthage update` and add it to your project. ### Step 2: Install the Latest versions of 1Password & Xcode -The sample project depends upon having the latest version of Xcode 6, as well as the latest version of 1Password installed on your iOS device. +The sample project depends upon having the latest version of Xcode, as well as the latest version of 1Password installed on your iOS device. -To install 1Password, you will need to download it from the [App Store](http://j.mp/1PasSITE). +To install 1Password, you will need to download it from the [App Store](https://1pw.ca/download/ios). Let us know that you're an app developer and planning to add 1Password support by emailing us to [support+appex@agilebits.com](mailto:support+appex@agilebits.com). ### Step 3: Run the Apps -Open `1Password Extension Demos` Xcode workspace from within the `Demos` folder with Xcode 6, and then select the `ACME` target and set it to run your iOS device: +Open `1Password Extension Demos` Xcode workspace from within the `Demos` folder with Xcode, and then select the `ACME` target and set it to run your iOS device: - + Since you will not have 1Password running within your iOS Simulator, it is important that you run on your device. @@ -74,13 +78,13 @@ Be forewarned, however, that there is not much code to get dirty with. If you we ### Add 1Password Files to Your Project -Add the `OnePasswordExtension.h`, `OnePasswordExtension.m`, and `1Password.xcassets` to your project and import `OnePasswordExtension.h` in your view contoller that implements the action for the 1Password button. +Add the `OnePasswordExtension.h`, `OnePasswordExtension.m`, and `1Password.xcassets` to your project and import `OnePasswordExtension.h` in your view controller that implements the action for the 1Password button. - + ### Use Case #1: Native App Login -In this use case we'll learn how to enable your existing users to fill their credentials into your native app's login form. If your application is using a web view to login (i.e. OAuth), you'll want to follow the web view integration steps in [Use Case #4: Web View Filling Support](https://github.com/AgileBits/onepassword-app-extension#use-case-4-web-view-filling-support). +In this use case we'll learn how to enable your existing users to fill their credentials into your native app's login form. If your application is using a web view to login (i.e. OAuth), you'll want to follow the web view integration steps in [Use Case #4: Web View Filling](https://github.com/AgileBits/onepassword-app-extension#use-case-4-web-view-filling-support). The first step is to add a UIButton to your login page. Use an existing 1Password image from the _1Password.xcassets_ catalog so users recognize the button. @@ -95,6 +99,12 @@ You'll need to hide this button (or educate users on the benefits of strong, uni Note that `isAppExtensionAvailable` looks to see if any app is installed that supports the generic `org-appextension-feature-password-management` feature. Any application that supports password management actions can be used. +**Important:** `isAppExtensionAvailable` uses `- [UIApplication canOpenURL:]`. Since iOS 9 it is recommended that you add the custom URL scheme, `org-appextension-feature-password-management`, in your target's `info.plist` as follows: + + + +For more information about URL schemes in iOS 9, please refer to the [Privacy and Your Apps session](https://developer.apple.com/videos/play/wwdc2015/703/) from WWDC 2015 at around the the 9th minute mark. + Next we need to wire up the action for this button to this method in your UIViewController: ```objective-c @@ -141,13 +151,27 @@ Adding 1Password to your registration screen is very similar to adding 1Password // Add as many string fields as you please. } }; - - // Password generation options are optional, but are very handy in case you have strict rules about password lengths - NSDictionary *passwordGenerationOptions = @{ - AppExtensionGeneratedPasswordMinLengthKey: @(6), // The minimum value can be 4 or more - AppExtensionGeneratedPasswordMaxLengthKey: @(50) // The maximum value can be 50 or less - }; + + // The password generation options are optional, but are very handy in case you have strict rules about password lengths, symbols and digits. + NSDictionary *passwordGenerationOptions = @{ + // The minimum password length can be 4 or more. + AppExtensionGeneratedPasswordMinLengthKey: @(8), + // The maximum password length can be 50 or less. + AppExtensionGeneratedPasswordMaxLengthKey: @(30), + + // If YES, the 1Password will guarantee that the generated password will contain at least one digit (number between 0 and 9). Passing NO will not exclude digits from the generated password. + AppExtensionGeneratedPasswordRequireDigitsKey: @(YES), + + // If YES, the 1Password will guarantee that the generated password will contain at least one symbol (See the list below). Passing NO will not exclude symbols from the generated password. + AppExtensionGeneratedPasswordRequireSymbolsKey: @(YES), + + // Here are all the symbols available in the the 1Password Password Generator: + // !@#$%^&*()_-+=|[]{}'\";.,>?/~` + // The string for AppExtensionGeneratedPasswordForbiddenCharactersKey should contain the symbols and characters that you wish 1Password to exclude from the generated password. + AppExtensionGeneratedPasswordForbiddenCharactersKey: @"!@#$%/0lIO" + }; + [[OnePasswordExtension sharedExtension] storeLoginForURLString:@"https://www.acme.com" loginDetails:newLoginDetails passwordGenerationOptions:passwordGenerationOptions forViewController:self sender:sender completion:^(NSDictionary *loginDictionary, NSError *error) { if (loginDictionary.count == 0) { @@ -168,7 +192,7 @@ Adding 1Password to your registration screen is very similar to adding 1Password You'll notice that we're passing a lot more information into 1Password than just the `URLString` key used in the sign in example. This is because at the end of the password generation process, 1Password will create a brand new login and save it. It's not possible for 1Password to ask your app for additional information later on, so we pass in everything we can before showing the password generator screen. -An important thing to notice is the `AppExtensionURLStringKey` is set to the exact same value we used in the login scenario. This allows users to quickly find the login they saved for your app the next time they need to sign in. +An important thing to notice is that the `URLString` is set to the exact same value we used in the login scenario. This allows users to quickly find the login they saved for your app the next time they need to sign in. ### Use Case #3: Change Password @@ -180,20 +204,46 @@ Adding 1Password to your change password screen is very similar to adding 1Passw - (IBAction)changePasswordIn1Password:(id)sender { NSString *changedPassword = self.freshPasswordTextField.text ? : @""; NSString *oldPassword = self.oldPasswordTextField.text ? : @""; - NSString *username = [LoginInformation sharedLoginInformation].username ? : @""; - + NSString *confirmationPassword = self.confirmPasswordTextField.text ? : @""; + + // Validate that the new password and the old password are not the same. + if (oldPassword.length > 0 && [oldPassword isEqualToString:changedPassword]) { + [self showChangePasswordFailedAlertWithMessage:@"The old and the new password must not be the same"]; + return; + } + + // Validate that the new and confirmation passwords match. + if (NO == [changedPassword isEqualToString:confirmationPassword]) { + [self showChangePasswordFailedAlertWithMessage:@"The new passwords and the confirmation password must match"]; + return; + } + NSDictionary *loginDetails = @{ - AppExtensionTitleKey: @"ACME", - AppExtensionUsernameKey: username, // 1Password will prompt the user to create a new item if no matching logins are found with this username. + AppExtensionTitleKey: @"ACME", // Optional, used for the third schenario only + AppExtensionUsernameKey: @"aUsername", // Optional, used for the third schenario only AppExtensionPasswordKey: changedPassword, AppExtensionOldPasswordKey: oldPassword, - AppExtensionNotesKey: @"Saved with the ACME app", + AppExtensionNotesKey: @"Saved with the ACME app", // Optional, used for the third schenario only }; - // Password generation options are optional, but are very handy in case you have strict rules about password lengths - NSDictionary *passwordGenerationOptions = @{ - AppExtensionGeneratedPasswordMinLengthKey: @(6), // The minimum value can be 4 or more - AppExtensionGeneratedPasswordMaxLengthKey: @(50) // The maximum value can be 50 or less + // The password generation options are optional, but are very handy in case you have strict rules about password lengths, symbols and digits. + NSDictionary *passwordGenerationOptions = @{ + // The minimum password length can be 4 or more. + AppExtensionGeneratedPasswordMinLengthKey: @(8), + + // The maximum password length can be 50 or less. + AppExtensionGeneratedPasswordMaxLengthKey: @(30), + + // If YES, the 1Password will guarantee that the generated password will contain at least one digit (number between 0 and 9). Passing NO will not exclude digits from the generated password. + AppExtensionGeneratedPasswordRequireDigitsKey: @(YES), + + // If YES, the 1Password will guarantee that the generated password will contain at least one symbol (See the list below). Passing NO will not exclude symbols from the generated password. + AppExtensionGeneratedPasswordRequireSymbolsKey: @(YES), + + // Here are all the symbols available in the the 1Password Password Generator: + // !@#$%^&*()_-+=|[]{}'\";.,>?/~` + // The string for AppExtensionGeneratedPasswordForbiddenCharactersKey should contain the symbols and characters that you wish 1Password to exclude from the generated password. + AppExtensionGeneratedPasswordForbiddenCharactersKey: @"!@#$%/0lIO" }; [[OnePasswordExtension sharedExtension] changePasswordForLoginForURLString:@"https://www.acme.com" loginDetails:loginDetails passwordGenerationOptions:passwordGenerationOptions forViewController:self sender:sender completion:^(NSDictionary *loginDictionary, NSError *error) { @@ -211,9 +261,9 @@ Adding 1Password to your change password screen is very similar to adding 1Passw } ``` -### Use Case #4: Web View Filling Support +### Use Case #4: Web View Filling -The 1Password App Extension is not limited to filling native UIs. With just a little bit of extra effort, users can fill `UIWebView`s and `WKWebView`s within your application as well. +The 1Password App Extension is not limited to filling native UIs. With just a little bit of extra effort, users can fill `WKWebView`s within your application as well. Simply add a button to your UI with its action assigned to this method in your web view's UIViewController: @@ -231,25 +281,17 @@ Simply add a button to your UI with its action assigned to this method in your w If you use a web view to login (i.e. OAuth) and you do not want other activities to show up in the share sheet and other item categories (Credit Cards and Identities) to show up in the 1Password Extension, you need to pass `YES` for `showOnlyLogins`. -## Projects supporting iOS 7.1 and earlier - -If your project's Deployment Target is earlier than iOS 8.0, please make sure that you link to the `MobileCoreServices` and `WebKit` frameworks. - - - -#### WKWebView support for projects with iOS 7.1 or earler as the Deployment Target - -If the **Deployment Target** is `7.1` or earlier in your project or target and you are using `WKWebViews` (runtime checks for iOS 8 deveices), you simply need to add `ONE_PASSWORD_EXTENSION_ENABLE_WK_WEB_VIEW=1` to your `Preprocessor Macros`. - - +#### SFSafariViewController +If your app uses `SFSafariViewController`, the 1Password App Extension will show up in the share sheet on devices running iOS 9.2 or later just like it does in Safari. No implementation is required. + ## Best Practices * Use the same `URLString` during Registration and Login. * Ensure your `URLString` is set to your actual service so your users can easily find their logins within the main 1Password app. * You should only ask for the login information of your own service or one specific to your app. Giving a URL for a service which you do not own or support may seriously break the customer's trust in your service/app. * If you don't have a website for your app you should specify your bundle identifier as the `URLString`, like so: `app://bundleIdentifier` (e.g. `app://com.acme.awesome-app`). -* [Send us an icon](mailto:support+appex@agilebits.com) to use for our Rich Icon service so the user can see your lovely icon while creating new items. +* [Send us an icon](mailto:support+appex@agilebits.com) to use for our Rich Icon service so the user can see your lovely icon after creating new items. Please send an icon that is 1024x1024px. Make sure that you also include the URL string that you used, so we can associate it with the icon on our Rich Icons server. * Use the icons provided in the `1Password.xcassets` asset catalog so users are familiar with what it will do. Contact us if you'd like additional sizes or have other special requirements. * Enable users to set 1Password as their default browser for external web links. * On your registration page, pre-validate fields before calling 1Password. For example, display a message if the username is not available so the user can fix it before calling the 1Password extension. @@ -259,12 +301,12 @@ If the **Deployment Target** is `7.1` or earlier in your project or target and y If you open up OnePasswordExtension.m and start poking around, you'll be interested in these references. -* [Apple Extension Guide](https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/ExtensibilityPG/index.html#//apple_ref/doc/uid/TP40014214) -* [NSItemProvider](https://developer.apple.com/library/prerelease/ios/documentation/Foundation/Reference/NSItemProvider_Class/index.html#//apple_ref/doc/uid/TP40014351), [NSExtensionItem](https://developer.apple.com/library/prerelease/ios/documentation/Foundation/Reference/NSExtensionItem_Class/index.html#//apple_ref/doc/uid/TP40014375), and [UIActivityViewController](https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIActivityViewController_Class/index.html#//apple_ref/doc/uid/TP40011976) class references. +* [Apple Extension Guide](https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/index.html#//apple_ref/doc/uid/TP40014214) +* [NSItemProvider](https://developer.apple.com/documentation/foundation/nsitemprovider#//apple_ref/doc/uid/TP40014351), [NSExtensionItem](https://developer.apple.com/documentation/foundation/nsextensionitem#//apple_ref/doc/uid/TP40014375), and [UIActivityViewController](https://developer.apple.com/documentation/uikit/uiactivityviewcontroller#//apple_ref/doc/uid/TP40011976) class references. ## Contact Us Contact us, please! We'd love to hear from you about how you integrated 1Password within your app, how we can further improve things, and add your app to [apps that integrate with 1Password](https://blog.agilebits.com/1password-apps/). -You can reach us at support+appex@agilebits.com, or if you prefer, [@1PasswordBeta](https://twitter.com/1PasswordBeta) on Twitter. +You can reach us at support+appex@agilebits.com, or if you prefer, [@1Password](https://twitter.com/1Password) on Twitter.