From 19233463b8f1eb3d2bff17a81aeb2eb80bdf7a80 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 10 Jun 2026 22:56:10 +1200 Subject: [PATCH 1/3] Remove the WordPressAuthenticator Xcode targets --- WordPress/WordPress.xcodeproj/project.pbxproj | 541 ------------------ 1 file changed, 541 deletions(-) diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index ccc9e0f7a4a2..0e60ef2e2901 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -48,8 +48,6 @@ 0C5A8A7D2D9B22F100C25301 /* React.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D38F2D2C4BA3008ACD86 /* React.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0C5A8A7E2D9B22F100C25301 /* RNTAztecView.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D3912D2C4BA3008ACD86 /* RNTAztecView.xcframework */; }; 0C5A8A7F2D9B22F100C25301 /* RNTAztecView.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D3912D2C4BA3008ACD86 /* RNTAztecView.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 0C5A8A802D9B22F100C25301 /* WordPressAuthenticator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; }; - 0C5A8A812D9B22F100C25301 /* WordPressAuthenticator.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0C5A8A842D9B22F100C25301 /* yoga.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D3932D2C4BA4008ACD86 /* yoga.xcframework */; }; 0C5A8A852D9B22F100C25301 /* yoga.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D3932D2C4BA4008ACD86 /* yoga.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0C5F80182E8E98A200D3F8EC /* XcodeTarget_UITests in Frameworks */ = {isa = PBXBuildFile; productRef = 0C5F80172E8E98A200D3F8EC /* XcodeTarget_UITests */; }; @@ -64,7 +62,6 @@ 0C6AC6222C364A7B00BF7600 /* XcodeTarget_NotificationServiceExtension in Frameworks */ = {isa = PBXBuildFile; productRef = 0C6AC6212C364A7B00BF7600 /* XcodeTarget_NotificationServiceExtension */; }; 0C6AC6242C364A8000BF7600 /* XcodeTarget_StatsWidget in Frameworks */ = {isa = PBXBuildFile; productRef = 0C6AC6232C364A8000BF7600 /* XcodeTarget_StatsWidget */; }; 0C6AC6262C364A8500BF7600 /* XcodeTarget_Intents in Frameworks */ = {isa = PBXBuildFile; productRef = 0C6AC6252C364A8500BF7600 /* XcodeTarget_Intents */; }; - 0C6AC6282C364A9000BF7600 /* XcodeTarget_WordPressAuthentificator in Frameworks */ = {isa = PBXBuildFile; productRef = 0C6AC6272C364A9000BF7600 /* XcodeTarget_WordPressAuthentificator */; }; 0CCA99512DAD76AD0048F0A9 /* AppImages.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 433840C622C2BA5B00CB13F8 /* AppImages.xcassets */; }; 0CCA99592DAD76BA0048F0A9 /* Noticons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F5A34D0C25DF2F7700C9654B /* Noticons.ttf */; }; 0CCA995A2DAD76C20048F0A9 /* n.caf in Resources */ = {isa = PBXBuildFile; fileRef = 5D69DBC3165428CA00A2D1F7 /* n.caf */; }; @@ -75,15 +72,11 @@ 0CECA92D2E043D0200F4EE83 /* XcodeTarget_App in Frameworks */ = {isa = PBXBuildFile; productRef = 0CECA92C2E043D0200F4EE83 /* XcodeTarget_App */; }; 0CECA92E2E043D2800F4EE83 /* WordPressData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F7AE0B52D9B30A100AB4892 /* WordPressData.framework */; }; 0CECA92F2E043D2800F4EE83 /* WordPressData.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3F7AE0B52D9B30A100AB4892 /* WordPressData.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 0CECA9332E043D3700F4EE83 /* WordPressAuthenticator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; }; - 0CECA9342E043D3700F4EE83 /* WordPressAuthenticator.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0CED2AD92D95BB46003015CF /* Gutenberg.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D3902D2C4BA3008ACD86 /* Gutenberg.xcframework */; }; 0CED2ADB2D95BB46003015CF /* hermes.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D3922D2C4BA3008ACD86 /* hermes.xcframework */; }; 0CED2ADD2D95BB46003015CF /* React.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D38F2D2C4BA3008ACD86 /* React.xcframework */; }; 0CED2ADF2D95BB46003015CF /* RNTAztecView.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D3912D2C4BA3008ACD86 /* RNTAztecView.xcframework */; }; - 0CED2AE12D95BB46003015CF /* WordPressAuthenticator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; }; 0CED2AE52D95BB46003015CF /* yoga.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D3932D2C4BA4008ACD86 /* yoga.xcframework */; }; - 0CFFFECB2C36F5760044709B /* XcodeTarget_WordPressAuthentificatorTests in Frameworks */ = {isa = PBXBuildFile; productRef = 0CFFFECA2C36F5760044709B /* XcodeTarget_WordPressAuthentificatorTests */; }; 1D60589F0D05DD5A006BFB54 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D30AB110D05D00D00671497 /* Foundation.framework */; }; 24351254264DCA08009BB2B6 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24351253264DCA08009BB2B6 /* Secrets.swift */; }; 24351255264DCA08009BB2B6 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24351253264DCA08009BB2B6 /* Secrets.swift */; }; @@ -125,8 +118,6 @@ 433840C722C2BA5B00CB13F8 /* AppImages.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 433840C622C2BA5B00CB13F8 /* AppImages.xcassets */; }; 433840C822C2BA6300CB13F8 /* AppImages.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 433840C622C2BA5B00CB13F8 /* AppImages.xcassets */; }; 433840C922C2BA6400CB13F8 /* AppImages.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 433840C622C2BA5B00CB13F8 /* AppImages.xcassets */; }; - 4A0274862C224FB000290D8B /* WordPressAuthenticator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; }; - 4A0274872C224FB000290D8B /* WordPressAuthenticator.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4A690C152BA791B100A8E0C5 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 4A690C142BA790BC00A8E0C5 /* PrivacyInfo.xcprivacy */; }; 4A690C162BA791B200A8E0C5 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 4A690C142BA790BC00A8E0C5 /* PrivacyInfo.xcprivacy */; }; 4A690C182BA794C800A8E0C5 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 4A690C172BA794C300A8E0C5 /* PrivacyInfo.xcprivacy */; }; @@ -145,10 +136,6 @@ 4ABCAB3A2DE533A5005A6B84 /* Secrets-WordPressNotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ABCAB392DE533A5005A6B84 /* Secrets-WordPressNotificationServiceExtension.swift */; }; 4AC9545A2DE51FE40095EA51 /* Secrets-JetpackStatsWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC954592DE51FE40095EA51 /* Secrets-JetpackStatsWidgets.swift */; }; 4AC9F8182DE528E40095EA51 /* Secrets-WordPressShareExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC9F8172DE528E40095EA51 /* Secrets-WordPressShareExtension.swift */; }; - 4AD953C72C21451700D0EEFA /* WordPressAuthenticator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; }; - 4AD953C82C21451700D0EEFA /* WordPressAuthenticator.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 4AD9555A2C21716A00D0EEFA /* WordPressAuthenticator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; }; - 4AD9555B2C21716A00D0EEFA /* WordPressAuthenticator.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 5D69DBC4165428CA00A2D1F7 /* n.caf in Resources */ = {isa = PBXBuildFile; fileRef = 5D69DBC3165428CA00A2D1F7 /* n.caf */; }; 7335AC6021220D550012EF2D /* RemoteNotificationActionParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7335AC5F21220D550012EF2D /* RemoteNotificationActionParser.swift */; }; 7358E6BF210BD318002323EB /* WordPressNotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7358E6B8210BD318002323EB /* WordPressNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -294,13 +281,6 @@ remoteGlobalIDString = 0CED016F2D95B897003015CF; remoteInfo = Keystone; }; - 0C5A8A822D9B22F100C25301 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; - proxyType = 1; - remoteGlobalIDString = 4AD953B32C21451700D0EEFA; - remoteInfo = WordPressAuthenticator; - }; 0CECA9302E043D2800F4EE83 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; @@ -308,20 +288,6 @@ remoteGlobalIDString = 3F7AE0B42D9B30A100AB4892; remoteInfo = WordPressData; }; - 0CECA9352E043D3700F4EE83 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; - proxyType = 1; - remoteGlobalIDString = 4AD953B32C21451700D0EEFA; - remoteInfo = WordPressAuthenticator; - }; - 0CED2AE32D95BB46003015CF /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; - proxyType = 1; - remoteGlobalIDString = 4AD953B32C21451700D0EEFA; - remoteInfo = WordPressAuthenticator; - }; 3F0FD9FE2D9B92F700CD05D6 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; @@ -364,27 +330,6 @@ remoteGlobalIDString = 1D6058900D05DD3D006BFB54; remoteInfo = WordPress; }; - 4AD953BD2C21451700D0EEFA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; - proxyType = 1; - remoteGlobalIDString = 4AD953B32C21451700D0EEFA; - remoteInfo = WordPressAuthenticator; - }; - 4AD953C52C21451700D0EEFA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; - proxyType = 1; - remoteGlobalIDString = 4AD953B32C21451700D0EEFA; - remoteInfo = WordPressAuthenticator; - }; - 4AD9555C2C21716A00D0EEFA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; - proxyType = 1; - remoteGlobalIDString = 4AD953B32C21451700D0EEFA; - remoteInfo = WordPressAuthenticator; - }; 7358E6BD210BD318002323EB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; @@ -479,7 +424,6 @@ files = ( 0C5A8A752D9B22F100C25301 /* Gutenberg.xcframework in Embed Frameworks */, 0C5A8A792D9B22F100C25301 /* WordPress.framework in Embed Frameworks */, - 0C5A8A812D9B22F100C25301 /* WordPressAuthenticator.framework in Embed Frameworks */, 0C5A8A772D9B22F100C25301 /* hermes.xcframework in Embed Frameworks */, 0C5A8A852D9B22F100C25301 /* yoga.xcframework in Embed Frameworks */, 0C5A8A7D2D9B22F100C25301 /* React.xcframework in Embed Frameworks */, @@ -559,39 +503,17 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; - 0C68E7832C35F9320023DB42 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; 0CECA9322E043D2800F4EE83 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( - 0CECA9342E043D3700F4EE83 /* WordPressAuthenticator.framework in Embed Frameworks */, 0CECA92F2E043D2800F4EE83 /* WordPressData.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; - 4A0274882C224FB000290D8B /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 4A0274872C224FB000290D8B /* WordPressAuthenticator.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; 4AD953C92C21451800D0EEFA /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -600,7 +522,6 @@ files = ( 3F60D3972D2C4BA4008ACD86 /* Gutenberg.xcframework in Embed Frameworks */, 3F60D3992D2C4BA4008ACD86 /* RNTAztecView.xcframework in Embed Frameworks */, - 4AD953C82C21451700D0EEFA /* WordPressAuthenticator.framework in Embed Frameworks */, 3F60D3952D2C4BA4008ACD86 /* React.xcframework in Embed Frameworks */, 3F0FD9FD2D9B92F700CD05D6 /* WordPressData.framework in Embed Frameworks */, 3F60D39B2D2C4BA4008ACD86 /* hermes.xcframework in Embed Frameworks */, @@ -615,7 +536,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 4AD9555B2C21716A00D0EEFA /* WordPressAuthenticator.framework in Embed Frameworks */, 3F608F822D2D1A9E008ACD86 /* hermes.xcframework in Embed Frameworks */, 3F608F802D2D1A9E008ACD86 /* Gutenberg.xcframework in Embed Frameworks */, 3F608F842D2D1A9E008ACD86 /* React.xcframework in Embed Frameworks */, @@ -790,8 +710,6 @@ 4ABCAB392DE533A5005A6B84 /* Secrets-WordPressNotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Secrets-WordPressNotificationServiceExtension.swift"; path = "../Secrets/Secrets-WordPressNotificationServiceExtension.swift"; sourceTree = BUILT_PRODUCTS_DIR; }; 4AC954592DE51FE40095EA51 /* Secrets-JetpackStatsWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Secrets-JetpackStatsWidgets.swift"; path = "../Secrets/Secrets-JetpackStatsWidgets.swift"; sourceTree = BUILT_PRODUCTS_DIR; }; 4AC9F8172DE528E40095EA51 /* Secrets-WordPressShareExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Secrets-WordPressShareExtension.swift"; path = "../Secrets/Secrets-WordPressShareExtension.swift"; sourceTree = BUILT_PRODUCTS_DIR; }; - 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WordPressAuthenticator.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 4AD953BB2C21451700D0EEFA /* WordPressAuthenticatorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WordPressAuthenticatorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 5D69DBC3165428CA00A2D1F7 /* n.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = n.caf; path = Resources/Sounds/n.caf; sourceTree = ""; }; 6EDC0E8E105881A800F68A1D /* iTunesArtwork */ = {isa = PBXFileReference; lastKnownFileType = file; path = iTunesArtwork; sourceTree = ""; }; 7335AC5F21220D550012EF2D /* RemoteNotificationActionParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteNotificationActionParser.swift; sourceTree = ""; }; @@ -949,17 +867,6 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 0C2390242D9ADFAA00981631 /* Exceptions for "WordPressAuthenticator" folder in "WordPressAuthenticator" target */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - publicHeaders = ( - Features/NUX/WPNUXMainButton.h, - Features/NUX/WPWalkthroughTextField.h, - Helpers/LoginFacade.h, - Helpers/WordPressXMLRPCAPIFacade.h, - WordPressAuthenticator.h, - ); - target = 4AD953B32C21451700D0EEFA /* WordPressAuthenticator */; - }; 0C3CC0152DA058E6009F3BFB /* Exceptions for "WordPress" folder in "WordPress" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -1136,9 +1043,6 @@ }; 0C238F782D9ADF0200981631 /* WordPressAuthenticator */ = { isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - 0C2390242D9ADFAA00981631 /* Exceptions for "WordPressAuthenticator" folder in "WordPressAuthenticator" target */, - ); path = WordPressAuthenticator; sourceTree = ""; }; @@ -1277,7 +1181,6 @@ files = ( 0CECA92E2E043D2800F4EE83 /* WordPressData.framework in Frameworks */, 0CECA92B2E043CEC00F4EE83 /* XcodeTarget_App in Frameworks */, - 0CECA9332E043D3700F4EE83 /* WordPressAuthenticator.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1296,7 +1199,6 @@ 0C5A8A742D9B22F100C25301 /* Gutenberg.xcframework in Frameworks */, 0C28A01C2DAD7CFD00F81F20 /* XcodeTarget_App in Frameworks */, 0C5A8A782D9B22F100C25301 /* WordPress.framework in Frameworks */, - 0C5A8A802D9B22F100C25301 /* WordPressAuthenticator.framework in Frameworks */, 0C5A8A762D9B22F100C25301 /* hermes.xcframework in Frameworks */, 0C5A8A842D9B22F100C25301 /* yoga.xcframework in Frameworks */, 0C5A8A7C2D9B22F100C25301 /* React.xcframework in Frameworks */, @@ -1310,7 +1212,6 @@ buildActionMask = 2147483647; files = ( 0CED2ADB2D95BB46003015CF /* hermes.xcframework in Frameworks */, - 0CED2AE12D95BB46003015CF /* WordPressAuthenticator.framework in Frameworks */, 0CED2AD92D95BB46003015CF /* Gutenberg.xcframework in Frameworks */, 0CED2AE52D95BB46003015CF /* yoga.xcframework in Frameworks */, 3F1AFCC22DA3AAEA00786B92 /* WebKit.framework in Frameworks */, @@ -1339,7 +1240,6 @@ 93E5285619A77BAC003A1A9C /* NotificationCenter.framework in Frameworks */, 93A3F7DE1843F6F00082FEEA /* CoreTelephony.framework in Frameworks */, A01C542E0E24E88400D411F2 /* SystemConfiguration.framework in Frameworks */, - 4AD953C72C21451700D0EEFA /* WordPressAuthenticator.framework in Frameworks */, 374CB16215B93C0800DD0EBC /* AudioToolbox.framework in Frameworks */, E10B3655158F2D7800419A93 /* CoreGraphics.framework in Frameworks */, 24E55D4F2CC9A5CD008D071D /* ImagePlayground.framework in Frameworks */, @@ -1391,23 +1291,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 4AD953B12C21451700D0EEFA /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 0C6AC6282C364A9000BF7600 /* XcodeTarget_WordPressAuthentificator in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 4AD953B82C21451700D0EEFA /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 0CFFFECB2C36F5760044709B /* XcodeTarget_WordPressAuthentificatorTests in Frameworks */, - 4A0274862C224FB000290D8B /* WordPressAuthenticator.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 7358E6B5210BD318002323EB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1499,7 +1382,6 @@ 3F608F852D2D1A9E008ACD86 /* RNTAztecView.xcframework in Frameworks */, FABB26302602FC2C00C8785C /* AVFoundation.framework in Frameworks */, FABB26312602FC2C00C8785C /* Foundation.framework in Frameworks */, - 4AD9555A2C21716A00D0EEFA /* WordPressAuthenticator.framework in Frameworks */, 3F608F872D2D1A9E008ACD86 /* yoga.xcframework in Frameworks */, FABB26322602FC2C00C8785C /* Security.framework in Frameworks */, FABB26332602FC2C00C8785C /* MapKit.framework in Frameworks */, @@ -1583,8 +1465,6 @@ 80F6D05428EE866A00953C1A /* JetpackNotificationServiceExtension.appex */, 0107E0EA28F97D5000DE87DB /* JetpackStatsWidgets.appex */, 0107E15428FE9DB200DE87DB /* JetpackIntents.appex */, - 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */, - 4AD953BB2C21451700D0EEFA /* WordPressAuthenticatorTests.xctest */, 0CED01702D95B897003015CF /* WordPress.framework */, 0C5A3F8C2D9B1E3700C25301 /* Reader.app */, 3F7AE0B52D9B30A100AB4892 /* WordPressData.framework */, @@ -1962,13 +1842,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 4AD953AF2C21451700D0EEFA /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ @@ -2032,7 +1905,6 @@ ); dependencies = ( 0CECA9312E043D2800F4EE83 /* PBXTargetDependency */, - 0CECA9362E043D3700F4EE83 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 0C3313B82E0439A8000C3760 /* Miniature */, @@ -2083,7 +1955,6 @@ ); dependencies = ( 0C5A8A7B2D9B22F100C25301 /* PBXTargetDependency */, - 0C5A8A832D9B22F100C25301 /* PBXTargetDependency */, 0C28C63C2DAD830F00F81F20 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( @@ -2111,7 +1982,6 @@ buildRules = ( ); dependencies = ( - 0CED2AE42D95BB46003015CF /* PBXTargetDependency */, 0C128FED2D9ECE6200C69EBA /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( @@ -2147,7 +2017,6 @@ 932225B01C7CE50300443B02 /* PBXTargetDependency */, 7457667B202B558C00F42E40 /* PBXTargetDependency */, 7358E6BE210BD318002323EB /* PBXTargetDependency */, - 4AD953C62C21451700D0EEFA /* PBXTargetDependency */, 3F0FD9FF2D9B92F700CD05D6 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( @@ -2236,56 +2105,6 @@ productReference = 4A8280FD2E5FE9B60037E180 /* WordPressKitTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - 4AD953B32C21451700D0EEFA /* WordPressAuthenticator */ = { - isa = PBXNativeTarget; - buildConfigurationList = 4AD953D22C21451800D0EEFA /* Build configuration list for PBXNativeTarget "WordPressAuthenticator" */; - buildPhases = ( - 4AD953AF2C21451700D0EEFA /* Headers */, - 4AD953B02C21451700D0EEFA /* Sources */, - 4AD953B12C21451700D0EEFA /* Frameworks */, - 4AD953B22C21451700D0EEFA /* Resources */, - 0C68E7832C35F9320023DB42 /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - fileSystemSynchronizedGroups = ( - 0C238F782D9ADF0200981631 /* WordPressAuthenticator */, - ); - name = WordPressAuthenticator; - packageProductDependencies = ( - 0C6AC6272C364A9000BF7600 /* XcodeTarget_WordPressAuthentificator */, - ); - productName = WordPressAuthenticator; - productReference = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; - productType = "com.apple.product-type.framework"; - }; - 4AD953BA2C21451700D0EEFA /* WordPressAuthenticatorTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 4AD953D32C21451800D0EEFA /* Build configuration list for PBXNativeTarget "WordPressAuthenticatorTests" */; - buildPhases = ( - 4AD953B72C21451700D0EEFA /* Sources */, - 4AD953B82C21451700D0EEFA /* Frameworks */, - 4AD953B92C21451700D0EEFA /* Resources */, - 4A0274882C224FB000290D8B /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 4AD953BE2C21451700D0EEFA /* PBXTargetDependency */, - ); - fileSystemSynchronizedGroups = ( - 0C5A1A042D9B080900C25301 /* WordPressAuthenticatorTests */, - ); - name = WordPressAuthenticatorTests; - packageProductDependencies = ( - 0CFFFECA2C36F5760044709B /* XcodeTarget_WordPressAuthentificatorTests */, - ); - productName = WordPressAuthenticatorTests; - productReference = 4AD953BB2C21451700D0EEFA /* WordPressAuthenticatorTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; 7358E6B7210BD318002323EB /* WordPressNotificationServiceExtension */ = { isa = PBXNativeTarget; buildConfigurationList = 7358E6C4210BD318002323EB /* Build configuration list for PBXNativeTarget "WordPressNotificationServiceExtension" */; @@ -2496,7 +2315,6 @@ 8096212728E5411400940A5D /* PBXTargetDependency */, 8096219028E55F8600940A5D /* PBXTargetDependency */, 80F6D05F28EE88FC00953C1A /* PBXTargetDependency */, - 4AD9555D2C21716A00D0EEFA /* PBXTargetDependency */, 3F0FDA032D9B930100CD05D6 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( @@ -2586,12 +2404,6 @@ CreatedOnToolsVersion = 16.4; TestTargetID = 1D6058900D05DD3D006BFB54; }; - 4AD953B32C21451700D0EEFA = { - CreatedOnToolsVersion = 15.4; - }; - 4AD953BA2C21451700D0EEFA = { - CreatedOnToolsVersion = 15.4; - }; 7358E6B7210BD318002323EB = { CreatedOnToolsVersion = 9.4.1; LastSwiftMigration = 1000; @@ -2713,8 +2525,6 @@ 0107E0B128F97D5000DE87DB /* JetpackStatsWidgets */, 0107E13828FE9DB200DE87DB /* JetpackIntents */, 0C5A3F8B2D9B1E3700C25301 /* Reader */, - 4AD953B32C21451700D0EEFA /* WordPressAuthenticator */, - 4AD953BA2C21451700D0EEFA /* WordPressAuthenticatorTests */, 0CED016F2D95B897003015CF /* Keystone */, 3F7AE0B42D9B30A100AB4892 /* WordPressData */, 3F7AE0BB2D9B30A200AB4892 /* WordPressDataTests */, @@ -2811,20 +2621,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 4AD953B22C21451700D0EEFA /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 4AD953B92C21451700D0EEFA /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 7358E6B6210BD318002323EB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -3362,20 +3158,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 4AD953B02C21451700D0EEFA /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 4AD953B72C21451700D0EEFA /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 7358E6B4210BD318002323EB /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -3507,26 +3289,11 @@ target = 0CED016F2D95B897003015CF /* Keystone */; targetProxy = 0C5A8A7A2D9B22F100C25301 /* PBXContainerItemProxy */; }; - 0C5A8A832D9B22F100C25301 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 4AD953B32C21451700D0EEFA /* WordPressAuthenticator */; - targetProxy = 0C5A8A822D9B22F100C25301 /* PBXContainerItemProxy */; - }; 0CECA9312E043D2800F4EE83 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 3F7AE0B42D9B30A100AB4892 /* WordPressData */; targetProxy = 0CECA9302E043D2800F4EE83 /* PBXContainerItemProxy */; }; - 0CECA9362E043D3700F4EE83 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 4AD953B32C21451700D0EEFA /* WordPressAuthenticator */; - targetProxy = 0CECA9352E043D3700F4EE83 /* PBXContainerItemProxy */; - }; - 0CED2AE42D95BB46003015CF /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 4AD953B32C21451700D0EEFA /* WordPressAuthenticator */; - targetProxy = 0CED2AE32D95BB46003015CF /* PBXContainerItemProxy */; - }; 3F0FD9FF2D9B92F700CD05D6 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 3F7AE0B42D9B30A100AB4892 /* WordPressData */; @@ -3557,21 +3324,6 @@ target = 1D6058900D05DD3D006BFB54 /* WordPress */; targetProxy = 4A8281012E5FE9B60037E180 /* PBXContainerItemProxy */; }; - 4AD953BE2C21451700D0EEFA /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 4AD953B32C21451700D0EEFA /* WordPressAuthenticator */; - targetProxy = 4AD953BD2C21451700D0EEFA /* PBXContainerItemProxy */; - }; - 4AD953C62C21451700D0EEFA /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 4AD953B32C21451700D0EEFA /* WordPressAuthenticator */; - targetProxy = 4AD953C52C21451700D0EEFA /* PBXContainerItemProxy */; - }; - 4AD9555D2C21716A00D0EEFA /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 4AD953B32C21451700D0EEFA /* WordPressAuthenticator */; - targetProxy = 4AD9555C2C21716A00D0EEFA /* PBXContainerItemProxy */; - }; 7358E6BE210BD318002323EB /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 7358E6B7210BD318002323EB /* WordPressNotificationServiceExtension */; @@ -5183,271 +4935,6 @@ }; name = "Release-Alpha"; }; - 4AD953CA2C21451800D0EEFA /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 3F9DD3F62CC214BF00DF1760 /* Common.debug.xcconfig */; - buildSettings = { - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = ""; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_DYNAMIC_NO_PIC = NO; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 WordPress. All rights reserved."; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; - MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = org.wordpress.library.WordPressAuthenticator; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - 4AD953CB2C21451800D0EEFA /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 0C96AC202D92FC17000779B8 /* Common.release.xcconfig */; - buildSettings = { - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = ""; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 WordPress. All rights reserved."; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; - MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = org.wordpress.library.WordPressAuthenticator; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - 4AD953CD2C21451800D0EEFA /* Release-Alpha */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 3F9DD3F72CC2188400DF1760 /* Common.alpha.xcconfig */; - buildSettings = { - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = ""; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 WordPress. All rights reserved."; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; - MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = org.wordpress.library.WordPressAuthenticator; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = "Release-Alpha"; - }; - 4AD953CE2C21451800D0EEFA /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 3F9DD3F62CC214BF00DF1760 /* Common.debug.xcconfig */; - buildSettings = { - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Manual; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_DYNAMIC_NO_PIC = NO; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GENERATE_INFOPLIST_FILE = YES; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = org.wordpress.library.WordPressAuthenticatorTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 4AD953CF2C21451800D0EEFA /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 0C96AC202D92FC17000779B8 /* Common.release.xcconfig */; - buildSettings = { - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Manual; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - ENABLE_NS_ASSERTIONS = NO; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GENERATE_INFOPLIST_FILE = YES; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = org.wordpress.library.WordPressAuthenticatorTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 4AD953D12C21451800D0EEFA /* Release-Alpha */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 3F9DD3F72CC2188400DF1760 /* Common.alpha.xcconfig */; - buildSettings = { - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - ENABLE_NS_ASSERTIONS = NO; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GENERATE_INFOPLIST_FILE = YES; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = org.wordpress.library.WordPressAuthenticatorTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = "Release-Alpha"; - }; 7358E6C0210BD318002323EB /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = F14B5F70208E648200439554 /* WordPress.debug.xcconfig */; @@ -7157,26 +6644,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 4AD953D22C21451800D0EEFA /* Build configuration list for PBXNativeTarget "WordPressAuthenticator" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 4AD953CA2C21451800D0EEFA /* Debug */, - 4AD953CB2C21451800D0EEFA /* Release */, - 4AD953CD2C21451800D0EEFA /* Release-Alpha */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 4AD953D32C21451800D0EEFA /* Build configuration list for PBXNativeTarget "WordPressAuthenticatorTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 4AD953CE2C21451800D0EEFA /* Debug */, - 4AD953CF2C21451800D0EEFA /* Release */, - 4AD953D12C21451800D0EEFA /* Release-Alpha */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 7358E6C4210BD318002323EB /* Build configuration list for PBXNativeTarget "WordPressNotificationServiceExtension" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -7360,10 +6827,6 @@ isa = XCSwiftPackageProductDependency; productName = XcodeTarget_Intents; }; - 0C6AC6272C364A9000BF7600 /* XcodeTarget_WordPressAuthentificator */ = { - isa = XCSwiftPackageProductDependency; - productName = XcodeTarget_WordPressAuthentificator; - }; 0CECA92A2E043CEC00F4EE83 /* XcodeTarget_App */ = { isa = XCSwiftPackageProductDependency; productName = XcodeTarget_App; @@ -7372,10 +6835,6 @@ isa = XCSwiftPackageProductDependency; productName = XcodeTarget_App; }; - 0CFFFECA2C36F5760044709B /* XcodeTarget_WordPressAuthentificatorTests */ = { - isa = XCSwiftPackageProductDependency; - productName = XcodeTarget_WordPressAuthentificatorTests; - }; 3F0F25A52D9BDB5800CD05D6 /* XcodeTarget_WordPressData */ = { isa = XCSwiftPackageProductDependency; productName = XcodeTarget_WordPressData; From f673734332396de4391b8398a4532519996ec113 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 10 Jun 2026 23:02:01 +1200 Subject: [PATCH 2/3] Delete the WordPressAuthenticator library Removes the library sources and tests, the navigator groups, the scheme and test plan references, the Swift package manifest entries, and the localization and lint configuration entries. The library's two features are fully replaced by the web-based WordPress.com sign-in and the application password sign-in flow. --- .rubocop.yml | 1 - Modules/Package.swift | 22 - .../FancyAlertViewController+LoginError.swift | 171 -- .../Extensions/NSObject+Helpers.swift | 12 - .../Extensions/String+Underline.swift | 26 - .../Extensions/UIButton+Styles.swift | 16 - .../Extensions/UIImage+Assets.swift | 44 - .../Extensions/UIPasteboard+Detect.swift | 79 - .../Extensions/UIStoryboard+Helpers.swift | 30 - .../Extensions/UITableView+Helpers.swift | 20 - .../Extensions/UIView+AuthHelpers.swift | 18 - .../UIViewController+Dismissal.swift | 22 - .../Extensions/UIViewController+Helpers.swift | 11 - .../Extensions/URL+JetpackConnect.swift | 7 - .../Extensions/WPStyleGuide+Login.swift | 343 ---- .../EmailClientPicker/AppSelector.swift | 130 -- .../EmailClientPicker/LinkMailPresenter.swift | 48 - .../EmailClientPicker/URLHandler.swift | 15 - .../Features/NUX/Button/NUXButton.swift | 266 --- .../NUX/Button/NUXButtonView.storyboard | 206 --- .../NUX/Button/NUXButtonViewController.swift | 315 ---- .../NUXStackedButtonsViewController.swift | 263 --- .../NUX/ModalViewControllerPresenting.swift | 7 - .../Features/NUX/NUXKeyboardResponder.swift | 149 -- .../NUX/NUXLinkAuthViewController.swift | 44 - .../NUX/NUXLinkMailViewController.swift | 128 -- .../NUX/NUXNavigationController.swift | 7 - .../Features/NUX/NUXTableViewController.swift | 47 - .../Features/NUX/NUXViewController.swift | 86 - .../Features/NUX/NUXViewControllerBase.swift | 223 --- .../Features/NUX/WPNUXMainButton.h | 8 - .../Features/NUX/WPNUXMainButton.m | 81 - .../Features/NUX/WPWalkthroughTextField.h | 43 - .../Features/NUX/WPWalkthroughTextField.m | 297 ---- .../Features/SignIn/AppleAuthenticator.swift | 291 ---- .../Features/SignIn/EmailMagicLink.storyboard | 165 -- .../Features/SignIn/Login.storyboard | 1481 ----------------- .../SignIn/Login2FAViewController.swift | 326 ---- .../SignIn/LoginEmailViewController.swift | 631 ------- .../LoginLinkRequestViewController.swift | 188 --- .../SignIn/LoginNavigationController.swift | 39 - ...ginPrologueLoginMethodViewController.swift | 132 -- .../LoginProloguePageViewController.swift | 98 -- ...inPrologueSignupMethodViewController.swift | 134 -- .../SignIn/LoginPrologueViewController.swift | 739 -------- .../LoginSelfHostedViewController.swift | 281 ---- .../LoginSiteAddressViewController.swift | 345 ---- .../SignIn/LoginSocialErrorCell.swift | 94 -- .../LoginSocialErrorViewController.swift | 217 --- .../LoginUsernamePasswordViewController.swift | 245 --- .../Features/SignIn/LoginViewController.swift | 539 ------ .../SignIn/LoginWPComViewController.swift | 258 --- .../Features/SignIn/SigninEditingState.swift | 6 - .../SignIn/UIImageView+Additions.swift | 19 - .../Features/SignUp/Signup.storyboard | 186 --- .../SignUp/SignupEmailViewController.swift | 239 --- .../SignUp/SignupGoogleViewController.swift | 80 - .../SignUp/SignupNavigationController.swift | 8 - .../AuthenticatorAnalyticsTracker.swift | 513 ------ .../WordPressAuthenticator+Errors.swift | 15 - .../WordPressAuthenticator+Events.swift | 36 - ...WordPressAuthenticator+Notifications.swift | 11 - .../WordPressAuthenticator.swift | 547 ------ .../WordPressAuthenticatorConfiguration.swift | 262 --- ...rdPressAuthenticatorDelegateProtocol.swift | 198 --- .../WordPressAuthenticatorDisplayImages.swift | 19 - ...WordPressAuthenticatorDisplayStrings.swift | 258 --- .../WordPressAuthenticatorResult.swift | 27 - .../WordPressAuthenticatorStyles.swift | 305 ---- .../WordPressSupportSourceTag.swift | 84 - .../AuthenticatorCredentials.swift | 20 - .../Credentials/WordPressComCredentials.swift | 42 - .../Credentials/WordPressOrgCredentials.swift | 45 - ...ebAuthenticationSession+Utils.swift .swift | 20 - .../GoogleSignIn/Character+URLSafe.swift | 10 - .../Helpers/GoogleSignIn/Data+Base64URL.swift | 34 - .../Helpers/GoogleSignIn/Data+SHA256.swift | 12 - .../Helpers/GoogleSignIn/DataGetting.swift | 4 - .../Helpers/GoogleSignIn/GoogleClientId.swift | 26 - .../GoogleSignIn/GoogleOAuthTokenGetter.swift | 28 - .../GoogleOAuthTokenGetting.swift | 9 - .../Helpers/GoogleSignIn/IDToken.swift | 23 - .../Helpers/GoogleSignIn/JSONWebToken.swift | 47 - .../GoogleSignIn/NewGoogleAuthenticator.swift | 123 -- .../Helpers/GoogleSignIn/OAuthError.swift | 24 - .../OAuthRequestBody+GoogleSignIn.swift | 26 - .../GoogleSignIn/OAuthTokenRequestBody.swift | 45 - .../GoogleSignIn/OAuthTokenResponseBody.swift | 35 - .../ProofKeyForCodeExchange.swift | 120 -- .../GoogleSignIn/Result+ConvenienceInit.swift | 15 - .../GoogleSignIn/URL+GoogleSignIn.swift | 28 - .../URLRequest+GoogleSignIn.swift | 18 - .../GoogleSignIn/URLSesison+DataGetting.swift | 20 - .../Helpers/LoginFacade.h | 176 -- .../Helpers/LoginFacade.m | 179 -- .../Helpers/LoginFacade.swift | 111 -- .../Model/LoginFields+Validation.swift | 47 - .../Helpers/Model/LoginFields.swift | 143 -- .../Helpers/Model/LoginFieldsMeta.swift | 83 - .../Helpers/Model/WordPressComSiteInfo.swift | 61 - .../Helpers/Navigation/NavigateBack.swift | 16 - .../Navigation/NavigateToEnterAccount.swift | 31 - .../Navigation/NavigateToEnterSite.swift | 22 - .../NavigateToEnterSiteCredentials.swift | 29 - .../NavigateToEnterWPCOMPassword.swift | 29 - .../Helpers/Navigation/NavigateToRoot.swift | 16 - .../Navigation/NavigationCommand.swift | 10 - .../Helpers/SafariCredentialsService.swift | 92 - .../Helpers/SignupService.swift | 134 -- .../Helpers/SocialUser.swift | 8 - .../Helpers/SocialUserCreating.swift | 23 - .../UnifiedAuth/GoogleAuthenticator.swift | 442 ----- .../StoredCredentialsAuthenticator.swift | 247 --- .../UnifiedAuth/StoredCredentialsPicker.swift | 55 - .../ViewRelated/2FA/TwoFA.storyboard | 92 - .../ViewRelated/2FA/TwoFAViewController.swift | 694 -------- .../GetStarted/GetStarted.storyboard | 159 -- .../GetStarted/GetStartedViewController.swift | 987 ----------- .../ViewRelated/Google/GoogleAuth.storyboard | 58 - .../Google/GoogleAuthViewController.swift | 165 -- .../GoogleSignupConfirmation.storyboard | 92 - ...ogleSignupConfirmationViewController.swift | 256 --- .../Login/LoginMagicLink.storyboard | 92 - .../Login/LoginMagicLinkViewController.swift | 180 -- .../MagicLinkRequestedViewController.swift | 158 -- .../MagicLinkRequestedViewController.xib | 153 -- .../Login/MagicLinkRequester.swift | 28 - .../ViewRelated/Password/Password.storyboard | 111 -- .../Password/PasswordCoordinator.swift | 71 - .../Password/PasswordViewController.swift | 614 ------- .../GravatarEmailTableViewCell.swift | 106 -- .../GravatarEmailTableViewCell.xib | 88 - .../TextFieldTableViewCell.swift | 237 --- .../ReusableViews/TextFieldTableViewCell.xib | 67 - .../TextLabelTableViewCell.swift | 43 - .../ReusableViews/TextLabelTableViewCell.xib | 44 - .../TextLinkButtonTableViewCell.swift | 76 - .../TextLinkButtonTableViewCell.xib | 83 - .../TextWithLinkTableViewCell.swift | 43 - .../TextWithLinkTableViewCell.xib | 46 - .../SignupMagicLinkViewController.swift | 195 --- .../SignUp/UnifiedSignup.storyboard | 172 -- .../SignUp/UnifiedSignupViewController.swift | 249 --- .../SiteAddress/SiteAddress.storyboard | 172 -- ...SiteAddressViewController+Extensions.swift | 60 - .../SiteAddressViewController.swift | 681 -------- .../SiteAddress/SiteAddressViewModel.swift | 136 -- .../SiteCredentialsViewController.swift | 589 ------- .../VerifyEmail/VerifyEmail.storyboard | 107 -- .../VerifyEmailViewController.swift | 258 --- .../Helpers/WordPressComAccountService.swift | 103 -- .../Helpers/WordPressComBlogService.swift | 67 - .../WordPressComOAuthClientFacade.swift | 120 -- ...ordPressComOAuthClientFacadeProtocol.swift | 68 - .../Helpers/WordPressXMLRPCAPIFacade.h | 23 - .../Helpers/WordPressXMLRPCAPIFacade.m | 96 -- .../Resources/Animations/jetpack.json | 1 - .../Resources/Animations/notifications.json | 1 - .../Resources/Animations/post.json | 1 - .../Resources/Animations/reader.json | 1 - .../Resources/Animations/stats.json | 1 - .../Resources/Assets.xcassets/Contents.json | 6 - .../darkgrey-shadow.imageset/Contents.json | 23 - .../darkgrey-shadow.png | Bin 830 -> 0 bytes .../darkgrey-shadow@2x.png | Bin 887 -> 0 bytes .../darkgrey-shadow@3x.png | Bin 986 -> 0 bytes .../email.imageset/Contents.json | 15 - .../Assets.xcassets/email.imageset/email.pdf | Bin 7180 -> 0 bytes .../google.imageset/Contents.json | 23 - .../google.imageset/google.png | Bin 643 -> 0 bytes .../google.imageset/google@2x.png | Bin 1166 -> 0 bytes .../google.imageset/google@3x.png | Bin 1773 -> 0 bytes .../Contents.json | 12 - .../icon-password-field.pdf | Bin 4050 -> 0 bytes .../Contents.json | 12 - .../icon-post-search-highlight.pdf | Bin 4126 -> 0 bytes .../icon-url-field.imageset/Contents.json | 16 - .../icon-url-field.pdf | Bin 4145 -> 0 bytes .../Contents.json | 12 - .../icon-username-field.pdf | Bin 3940 -> 0 bytes .../key-icon.imageset/Contents.json | 12 - .../Assets.xcassets/key-icon.imageset/key.pdf | Bin 3395 -> 0 bytes .../login-magic-link.imageset/Contents.json | 12 - .../login-magic-link.pdf | Bin 436869 -> 0 bytes .../phone-icon.imageset/Contents.json | 12 - .../phone-icon.imageset/phone.pdf | Bin 3127 -> 0 bytes .../Contents.json | 12 - .../example-domain.png | Bin 34937 -> 0 bytes .../Contents.json | 12 - .../social-signup-waiting.pdf | Bin 11401 -> 0 bytes .../SupportedEmailClients/EmailClients.plist | 18 - .../Views/CircularImageView.swift | 22 - .../Views/LoginTextField.swift | 77 - .../Views/SearchTableViewCell.swift | 160 -- .../Views/SearchTableViewCell.xib | 55 - .../Views/SiteInfoHeaderView.swift | 116 -- ...WebAuthenticationPresentationContext.swift | 15 - .../WordPressAuthenticator.h | 15 - .../WordPressUnitTests.xctestplan | 7 - .../Analytics/AnalyticsTrackerTests.swift | 296 ---- .../WordPressAuthenticator+TestsUtils.swift | 50 - ...rdPressAuthenticatorDisplayTextTests.swift | 24 - .../WordPressAuthenticatorTests.swift | 176 -- .../WordPressSourceTagTests.swift | 131 -- .../Credentials/CredentialsTests.swift | 127 -- .../EmailClientPicker/AppSelectorTests.swift | 74 - .../GoogleSignIn/Character+URLSafeTests.swift | 17 - .../GoogleSignIn/CodeVerifier+Fixture.swift | 12 - .../GoogleSignIn/CodeVerifierTests.swift | 62 - .../GoogleSignIn/Data+Base64URLTests.swift | 83 - .../GoogleSignIn/Data+SHA256Tests.swift | 22 - .../GoogleSignIn/DataGettingStub.swift | 25 - .../GoogleSignIn/GoogleClientIdTests.swift | 23 - .../GoogleOAuthTokenGetterTests.swift | 50 - .../GoogleOAuthTokenGettingStub.swift | 30 - .../GoogleSignIn/IDTokenTests.swift | 25 - .../GoogleSignIn/JSONWebToken+Fixtures.swift | 58 - .../GoogleSignIn/JWTokenTests.swift | 28 - .../NewGoogleAuthenticatorTests.swift | 105 -- .../OAuthRequestBody+GoogleSignInTests.swift | 22 - .../OAuthTokenRequestBodyTests.swift | 28 - .../OAuthTokenResponseBody+Fixture.swift | 15 - .../ProofKeyForCodeExchangeTests.swift | 31 - .../Result+ConvenienceInitTests.swift | 40 - .../GoogleSignIn/URL+GoogleSignInTests.swift | 96 -- .../URLRequest+GoogleSignInTests.swift | 33 - .../Logging/LoggingTests.m | 79 - .../Logging/LoggingTests.swift | 69 - .../MemoryManagementTests.swift | 60 - .../Mocks/MockNavigationController.swift | 10 - .../ModalViewControllerPresentingSpy.swift | 8 - .../WordPressAuthenticatorDelegateSpy.swift | 82 - .../WordpressAuthenticatorProvider.swift | 111 -- .../Model/LoginFieldsTests.swift | 29 - .../Model/LoginFieldsValidationTests.swift | 42 - .../Model/WordPressComSiteInfoTests.swift | 50 - .../NavigationToEnterAccountTests.swift | 17 - .../NavigationToEnterSiteTests.swift | 17 - .../Services/LoginFacadeTests.m | 269 --- .../SingIn/AppleAuthenticatorTests.swift | 107 -- .../SingIn/LoginViewControllerTests.swift | 33 - .../SingIn/SiteAddressViewModelTests.swift | 125 -- .../WordPressAuthenticator.xctestplan | 24 - WordPress/WordPress.xcodeproj/project.pbxproj | 12 - .../xcshareddata/xcschemes/WordPress.xcscheme | 11 - .../xcschemes/WordPressAuthenticator.xcscheme | 72 - fastlane/lanes/localization.rb | 1 - 247 files changed, 26348 deletions(-) delete mode 100644 Sources/WordPressAuthenticator/Extensions/FancyAlertViewController+LoginError.swift delete mode 100644 Sources/WordPressAuthenticator/Extensions/NSObject+Helpers.swift delete mode 100644 Sources/WordPressAuthenticator/Extensions/String+Underline.swift delete mode 100644 Sources/WordPressAuthenticator/Extensions/UIButton+Styles.swift delete mode 100644 Sources/WordPressAuthenticator/Extensions/UIImage+Assets.swift delete mode 100644 Sources/WordPressAuthenticator/Extensions/UIPasteboard+Detect.swift delete mode 100644 Sources/WordPressAuthenticator/Extensions/UIStoryboard+Helpers.swift delete mode 100644 Sources/WordPressAuthenticator/Extensions/UITableView+Helpers.swift delete mode 100644 Sources/WordPressAuthenticator/Extensions/UIView+AuthHelpers.swift delete mode 100644 Sources/WordPressAuthenticator/Extensions/UIViewController+Dismissal.swift delete mode 100644 Sources/WordPressAuthenticator/Extensions/UIViewController+Helpers.swift delete mode 100644 Sources/WordPressAuthenticator/Extensions/URL+JetpackConnect.swift delete mode 100644 Sources/WordPressAuthenticator/Extensions/WPStyleGuide+Login.swift delete mode 100644 Sources/WordPressAuthenticator/Features/EmailClientPicker/AppSelector.swift delete mode 100644 Sources/WordPressAuthenticator/Features/EmailClientPicker/LinkMailPresenter.swift delete mode 100644 Sources/WordPressAuthenticator/Features/EmailClientPicker/URLHandler.swift delete mode 100644 Sources/WordPressAuthenticator/Features/NUX/Button/NUXButton.swift delete mode 100644 Sources/WordPressAuthenticator/Features/NUX/Button/NUXButtonView.storyboard delete mode 100644 Sources/WordPressAuthenticator/Features/NUX/Button/NUXButtonViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/NUX/Button/NUXStackedButtonsViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/NUX/ModalViewControllerPresenting.swift delete mode 100644 Sources/WordPressAuthenticator/Features/NUX/NUXKeyboardResponder.swift delete mode 100644 Sources/WordPressAuthenticator/Features/NUX/NUXLinkAuthViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/NUX/NUXLinkMailViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/NUX/NUXNavigationController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/NUX/NUXTableViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/NUX/NUXViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/NUX/NUXViewControllerBase.swift delete mode 100644 Sources/WordPressAuthenticator/Features/NUX/WPNUXMainButton.h delete mode 100644 Sources/WordPressAuthenticator/Features/NUX/WPNUXMainButton.m delete mode 100644 Sources/WordPressAuthenticator/Features/NUX/WPWalkthroughTextField.h delete mode 100644 Sources/WordPressAuthenticator/Features/NUX/WPWalkthroughTextField.m delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/AppleAuthenticator.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/EmailMagicLink.storyboard delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/Login.storyboard delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/Login2FAViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/LoginEmailViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/LoginLinkRequestViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/LoginNavigationController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueLoginMethodViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/LoginProloguePageViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueSignupMethodViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/LoginSelfHostedViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/LoginSiteAddressViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/LoginSocialErrorCell.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/LoginSocialErrorViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/LoginUsernamePasswordViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/LoginViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/LoginWPComViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/SigninEditingState.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignIn/UIImageView+Additions.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignUp/Signup.storyboard delete mode 100644 Sources/WordPressAuthenticator/Features/SignUp/SignupEmailViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignUp/SignupGoogleViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Features/SignUp/SignupNavigationController.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Analytics/AuthenticatorAnalyticsTracker.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Errors.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Events.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Notifications.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorConfiguration.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDelegateProtocol.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDisplayImages.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDisplayStrings.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorResult.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorStyles.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressSupportSourceTag.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Credentials/AuthenticatorCredentials.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Credentials/WordPressComCredentials.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Credentials/WordPressOrgCredentials.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/ASWebAuthenticationSession+Utils.swift .swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Character+URLSafe.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Data+Base64URL.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Data+SHA256.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/DataGetting.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleClientId.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleOAuthTokenGetter.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleOAuthTokenGetting.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/IDToken.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/JSONWebToken.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/NewGoogleAuthenticator.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthError.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthRequestBody+GoogleSignIn.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthTokenRequestBody.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthTokenResponseBody.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/ProofKeyForCodeExchange.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Result+ConvenienceInit.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URL+GoogleSignIn.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URLRequest+GoogleSignIn.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URLSesison+DataGetting.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/LoginFacade.h delete mode 100644 Sources/WordPressAuthenticator/Helpers/LoginFacade.m delete mode 100644 Sources/WordPressAuthenticator/Helpers/LoginFacade.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Model/LoginFields+Validation.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Model/LoginFields.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Model/LoginFieldsMeta.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Model/WordPressComSiteInfo.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Navigation/NavigateBack.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterAccount.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterSite.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterSiteCredentials.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterWPCOMPassword.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToRoot.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/Navigation/NavigationCommand.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/SafariCredentialsService.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/SignupService.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/SocialUser.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/SocialUserCreating.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/GoogleAuthenticator.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/StoredCredentialsAuthenticator.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/StoredCredentialsPicker.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/2FA/TwoFA.storyboard delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/2FA/TwoFAViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/GetStarted/GetStarted.storyboard delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/GetStarted/GetStartedViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleAuth.storyboard delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleAuthViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleSignupConfirmation.storyboard delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleSignupConfirmationViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/LoginMagicLink.storyboard delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/LoginMagicLinkViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequestedViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequestedViewController.xib delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequester.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/Password.storyboard delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/PasswordCoordinator.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/PasswordViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/GravatarEmailTableViewCell.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/GravatarEmailTableViewCell.xib delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextFieldTableViewCell.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextFieldTableViewCell.xib delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLabelTableViewCell.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLabelTableViewCell.xib delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLinkButtonTableViewCell.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLinkButtonTableViewCell.xib delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextWithLinkTableViewCell.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextWithLinkTableViewCell.xib delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/SignupMagicLinkViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/UnifiedSignup.storyboard delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/UnifiedSignupViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddress.storyboard delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewController+Extensions.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewModel.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteCredentialsViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/VerifyEmail/VerifyEmail.storyboard delete mode 100644 Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/VerifyEmail/VerifyEmailViewController.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/WordPressComAccountService.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/WordPressComBlogService.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/WordPressComOAuthClientFacade.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/WordPressComOAuthClientFacadeProtocol.swift delete mode 100644 Sources/WordPressAuthenticator/Helpers/WordPressXMLRPCAPIFacade.h delete mode 100644 Sources/WordPressAuthenticator/Helpers/WordPressXMLRPCAPIFacade.m delete mode 100644 Sources/WordPressAuthenticator/Resources/Animations/jetpack.json delete mode 100644 Sources/WordPressAuthenticator/Resources/Animations/notifications.json delete mode 100644 Sources/WordPressAuthenticator/Resources/Animations/post.json delete mode 100644 Sources/WordPressAuthenticator/Resources/Animations/reader.json delete mode 100644 Sources/WordPressAuthenticator/Resources/Animations/stats.json delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/Contents.json delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/Contents.json delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/darkgrey-shadow.png delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/darkgrey-shadow@2x.png delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/darkgrey-shadow@3x.png delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/email.imageset/Contents.json delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/email.imageset/email.pdf delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/Contents.json delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/google.png delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/google@2x.png delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/google@3x.png delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-password-field.imageset/Contents.json delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-password-field.imageset/icon-password-field.pdf delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-post-search-highlight.imageset/Contents.json delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-post-search-highlight.imageset/icon-post-search-highlight.pdf delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-url-field.imageset/Contents.json delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-url-field.imageset/icon-url-field.pdf delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-username-field.imageset/Contents.json delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-username-field.imageset/icon-username-field.pdf delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/key-icon.imageset/Contents.json delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/key-icon.imageset/key.pdf delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/login-magic-link.imageset/Contents.json delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/login-magic-link.imageset/login-magic-link.pdf delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/phone-icon.imageset/Contents.json delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/phone-icon.imageset/phone.pdf delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/site-address-illustration.imageset/Contents.json delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/site-address-illustration.imageset/example-domain.png delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/social-signup-waiting.imageset/Contents.json delete mode 100644 Sources/WordPressAuthenticator/Resources/Assets.xcassets/social-signup-waiting.imageset/social-signup-waiting.pdf delete mode 100644 Sources/WordPressAuthenticator/Resources/SupportedEmailClients/EmailClients.plist delete mode 100644 Sources/WordPressAuthenticator/Views/CircularImageView.swift delete mode 100644 Sources/WordPressAuthenticator/Views/LoginTextField.swift delete mode 100644 Sources/WordPressAuthenticator/Views/SearchTableViewCell.swift delete mode 100644 Sources/WordPressAuthenticator/Views/SearchTableViewCell.xib delete mode 100644 Sources/WordPressAuthenticator/Views/SiteInfoHeaderView.swift delete mode 100644 Sources/WordPressAuthenticator/Views/WebAuthenticationPresentationContext.swift delete mode 100644 Sources/WordPressAuthenticator/WordPressAuthenticator.h delete mode 100644 Tests/WordPressAuthenticatorTests/Analytics/AnalyticsTrackerTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticator+TestsUtils.swift delete mode 100644 Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticatorDisplayTextTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticatorTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/Authenticator/WordPressSourceTagTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/Credentials/CredentialsTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/EmailClientPicker/AppSelectorTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/Character+URLSafeTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/CodeVerifier+Fixture.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/CodeVerifierTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/Data+Base64URLTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/Data+SHA256Tests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/DataGettingStub.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleClientIdTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleOAuthTokenGetterTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleOAuthTokenGettingStub.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/IDTokenTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/JSONWebToken+Fixtures.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/JWTokenTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/NewGoogleAuthenticatorTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthRequestBody+GoogleSignInTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthTokenRequestBodyTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthTokenResponseBody+Fixture.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/ProofKeyForCodeExchangeTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/Result+ConvenienceInitTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/URL+GoogleSignInTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/GoogleSignIn/URLRequest+GoogleSignInTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/Logging/LoggingTests.m delete mode 100644 Tests/WordPressAuthenticatorTests/Logging/LoggingTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/MemoryManagementTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/Mocks/MockNavigationController.swift delete mode 100644 Tests/WordPressAuthenticatorTests/Mocks/ModalViewControllerPresentingSpy.swift delete mode 100644 Tests/WordPressAuthenticatorTests/Mocks/WordPressAuthenticatorDelegateSpy.swift delete mode 100644 Tests/WordPressAuthenticatorTests/Mocks/WordpressAuthenticatorProvider.swift delete mode 100644 Tests/WordPressAuthenticatorTests/Model/LoginFieldsTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/Model/LoginFieldsValidationTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/Model/WordPressComSiteInfoTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/Navigation/NavigationToEnterAccountTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/Navigation/NavigationToEnterSiteTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/Services/LoginFacadeTests.m delete mode 100644 Tests/WordPressAuthenticatorTests/SingIn/AppleAuthenticatorTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/SingIn/LoginViewControllerTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/SingIn/SiteAddressViewModelTests.swift delete mode 100644 Tests/WordPressAuthenticatorTests/WordPressAuthenticator.xctestplan delete mode 100644 WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/WordPressAuthenticator.xcscheme diff --git a/.rubocop.yml b/.rubocop.yml index ac394adddb51..fd109537d10c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -2,7 +2,6 @@ AllCops: Exclude: - DerivedData/**/* - vendor/**/* - - WordPressAuthenticator/**/* - WordPressKit/**/* NewCops: enable diff --git a/Modules/Package.swift b/Modules/Package.swift index eb49cb186880..80168575fa30 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -373,11 +373,6 @@ enum XcodeSupport { .library(name: "XcodeTarget_WordPressKitTests", targets: ["XcodeTarget_WordPressKitTests"]), .library(name: "XcodeTarget_WordPressData", targets: ["XcodeTarget_WordPressData"]), .library(name: "XcodeTarget_WordPressDataTests", targets: ["XcodeTarget_WordPressDataTests"]), - .library(name: "XcodeTarget_WordPressAuthentificator", targets: ["XcodeTarget_WordPressAuthentificator"]), - .library( - name: "XcodeTarget_WordPressAuthentificatorTests", - targets: ["XcodeTarget_WordPressAuthentificatorTests"] - ), .library(name: "XcodeTarget_ShareExtension", targets: ["XcodeTarget_ShareExtension"]), .library(name: "XcodeTarget_DraftActionExtension", targets: ["XcodeTarget_DraftActionExtension"]), .library( @@ -391,18 +386,6 @@ enum XcodeSupport { } static var targets: [Target] { - let wordPresAuthentificatorDependencies: [Target.Dependency] = [ - "BuildSettingsKit", - "WordPressShared", - "WordPressUI", - "WordPressKit", - .product(name: "Gridicons", package: "Gridicons-iOS"), - .product(name: "NSURL-IDN", package: "NSURL-IDN"), - .product(name: "SVProgressHUD", package: "SVProgressHUD"), - .product(name: "Gravatar", package: "Gravatar-SDK-iOS"), - .product(name: "GravatarUI", package: "Gravatar-SDK-iOS") - ] - let shareAndDraftExtensionsDependencies: [Target.Dependency] = [ "AztecExtensions", "BuildSettingsKit", @@ -529,11 +512,6 @@ enum XcodeSupport { "WordPressKit" ] ), - .xcodeTarget("XcodeTarget_WordPressAuthentificator", dependencies: wordPresAuthentificatorDependencies), - .xcodeTarget( - "XcodeTarget_WordPressAuthentificatorTests", - dependencies: wordPresAuthentificatorDependencies + testDependencies - ), .xcodeTarget("XcodeTarget_ShareExtension", dependencies: shareAndDraftExtensionsDependencies), .xcodeTarget("XcodeTarget_DraftActionExtension", dependencies: shareAndDraftExtensionsDependencies), .xcodeTarget( diff --git a/Sources/WordPressAuthenticator/Extensions/FancyAlertViewController+LoginError.swift b/Sources/WordPressAuthenticator/Extensions/FancyAlertViewController+LoginError.swift deleted file mode 100644 index 2189cf311564..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/FancyAlertViewController+LoginError.swift +++ /dev/null @@ -1,171 +0,0 @@ -import UIKit -import SafariServices -import WordPressUI -import WordPressKit - -extension FancyAlertViewController { - private struct Strings { - static let OK = NSLocalizedString("OK", comment: "Ok button for dismissing alert helping users understand their site address") - static let moreHelp = NSLocalizedString("Need more help?", comment: "Title of the more help button on alert helping users understand their site address") - } - - typealias ButtonConfig = FancyAlertViewController.Config.ButtonConfig - - private static func defaultButton(onTap: (() -> ())? = nil) -> ButtonConfig { - return ButtonConfig(Strings.OK) { controller, _ in - controller.dismiss(animated: true, completion: { - onTap?() - }) - } - } - - // MARK: - Error Handling - - /// Get an alert for the specified error. - /// The view is configured differently depending on the kind of error. - /// - /// - Parameters: - /// - error: An NSError instance - /// - loginFields: A LoginFields instance. - /// - sourceTag: The sourceTag that is the context of the error. - /// - /// - Returns: A FancyAlertViewController instance. - /// - static func alertForError(_ originalError: Error, loginFields: LoginFields, sourceTag: WordPressSupportSourceTag) -> FancyAlertViewController { - let error = originalError as NSError - var message = error.localizedDescription - - WPLogError(message) - - if sourceTag == .jetpackLogin && error.domain == WordPressAuthenticator.errorDomain && error.code == NSURLErrorBadURL { - if WordPressAuthenticator.shared.delegate?.supportEnabled == true { - // TODO: Placeholder Jetpack login error message. Needs updating with final wording. 2017-06-15 Aerych. - message = NSLocalizedString("We're not able to connect to the Jetpack site at that URL. Contact us for assistance.", comment: "Error message shown when having trouble connecting to a Jetpack site.") - return alertForGenericErrorMessageWithHelpButton(message, loginFields: loginFields, sourceTag: sourceTag) - } - } - - if error.domain != WordPressOrgXMLRPCApi.errorDomain && error.code != NSURLErrorBadURL { - if WordPressAuthenticator.shared.delegate?.supportEnabled == true { - return alertForGenericErrorMessageWithHelpButton(message, loginFields: loginFields, sourceTag: sourceTag) - } - - return alertForGenericErrorMessage(message, loginFields: loginFields, sourceTag: sourceTag) - } - - if error.code == 403 { - message = NSLocalizedString("Incorrect username or password. Please try entering your login details again.", comment: "An error message shown when a user signed in with incorrect credentials.") - } - - if message.trim().isEmpty { - message = NSLocalizedString("Log in failed. Please try again.", comment: "A generic error message for a failed log in.") - } - - if error.code == NSURLErrorBadURL { - return alertForBadURL(with: message) - } - - return alertForGenericErrorMessage(message, loginFields: loginFields, sourceTag: sourceTag) - } - - /// Shows a generic error message. - /// - /// - Parameter message: The error message to show. - /// - private static func alertForGenericErrorMessage(_ message: String, loginFields: LoginFields, sourceTag: WordPressSupportSourceTag) -> FancyAlertViewController { - let moreHelpButton = ButtonConfig(Strings.moreHelp) { controller, _ in - controller.dismiss(animated: true) { - guard let sourceViewController = UIApplication.shared.delegate?.window??.topmostPresentedViewController, - let authDelegate = WordPressAuthenticator.shared.delegate - else { - return - } - - let state = AuthenticatorAnalyticsTracker.shared.state - authDelegate.presentSupport(from: sourceViewController, sourceTag: sourceTag, lastStep: state.lastStep, lastFlow: state.lastFlow) - } - } - - let config = FancyAlertViewController.Config(titleText: "", - bodyText: message, - headerImage: nil, - dividerPosition: .top, - defaultButton: defaultButton(), - cancelButton: nil, - moreInfoButton: moreHelpButton, - titleAccessoryButton: nil, - dismissAction: nil) - return FancyAlertViewController.controllerWithConfiguration(configuration: config) - } - - /// Shows a generic error message. - /// If Support is enabled, the view is configured so the user can open Support for assistance. - /// - /// - Parameter message: The error message to show. - /// - Parameter sourceTag: tag of the source of the error - /// - static func alertForGenericErrorMessageWithHelpButton(_ message: String, loginFields: LoginFields, sourceTag: WordPressSupportSourceTag, onDismiss: (() -> ())? = nil) -> FancyAlertViewController { - - // If support is not enabled, don't add a Help Button since it won't do anything. - var moreHelpButton: ButtonConfig? - - if WordPressAuthenticator.shared.delegate?.supportEnabled == false { - WPLogInfo("Error Alert: Support not enabled. Hiding Help button.") - } else { - moreHelpButton = ButtonConfig(Strings.moreHelp) { controller, _ in - controller.dismiss(animated: true) { - // Find the topmost view controller that we can present from - guard let appDelegate = UIApplication.shared.delegate, - let window = appDelegate.window, - let viewController = window?.topmostPresentedViewController, - WordPressAuthenticator.shared.delegate?.supportEnabled == true - else { - return - } - - WordPressAuthenticator.shared.delegate?.presentSupportRequest(from: viewController, sourceTag: sourceTag) - } - } - } - - let config = FancyAlertViewController.Config(titleText: "", - bodyText: message, - headerImage: nil, - dividerPosition: .top, - defaultButton: defaultButton(onTap: onDismiss), - cancelButton: nil, - moreInfoButton: moreHelpButton, - titleAccessoryButton: nil, - dismissAction: nil) - - return FancyAlertViewController.controllerWithConfiguration(configuration: config) - } - - private static func alertForBadURL(with message: String) -> FancyAlertViewController { - let moreHelpButton = ButtonConfig(Strings.moreHelp) { controller, _ in - controller.dismiss(animated: true) { - // Find the topmost view controller that we can present from - guard let viewController = UIApplication.shared.delegate?.window??.topmostPresentedViewController, - let url = URL(string: "https://apps.wordpress.org/support/#faq-ios-3") - else { - return - } - - let safariViewController = SFSafariViewController(url: url) - safariViewController.modalPresentationStyle = .pageSheet - viewController.present(safariViewController, animated: true, completion: nil) - } - } - - let config = FancyAlertViewController.Config(titleText: "", - bodyText: message, - headerImage: nil, - dividerPosition: .top, - defaultButton: defaultButton(), - cancelButton: nil, - moreInfoButton: moreHelpButton, - titleAccessoryButton: nil, - dismissAction: nil) - return FancyAlertViewController.controllerWithConfiguration(configuration: config) - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/NSObject+Helpers.swift b/Sources/WordPressAuthenticator/Extensions/NSObject+Helpers.swift deleted file mode 100644 index dddb827a6b50..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/NSObject+Helpers.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation - -// MARK: - NSObject Helper Methods -// -extension NSObject { - - /// Returns the receiver's classname as a string, not including the namespace. - /// - class var classNameWithoutNamespaces: String { - return String(describing: self) - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/String+Underline.swift b/Sources/WordPressAuthenticator/Extensions/String+Underline.swift deleted file mode 100644 index 0d836a9c8707..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/String+Underline.swift +++ /dev/null @@ -1,26 +0,0 @@ -import UIKit - -extension String { - /// Creates an attributed string from one underlined section that's surrounded by underscores - /// - /// - Parameters: - /// - color: foreground color to use for the string (optional) - /// - underlineColor: foreground color to use for the underlined section (optional) - /// - Returns: Attributed string - /// - Note: "this _is_ underlined" would under the "is" - func underlined(color: UIColor? = nil, underlineColor: UIColor? = nil) -> NSAttributedString { - let labelParts = self.components(separatedBy: "_") - let firstPart = labelParts[0] - let underlinePart = labelParts.indices.contains(1) ? labelParts[1] : "" - let lastPart = labelParts.indices.contains(2) ? labelParts[2] : "" - - let foregroundColor = color ?? UIColor.black - let underlineForegroundColor = underlineColor ?? foregroundColor - - let underlinedString = NSMutableAttributedString(string: firstPart, attributes: [.foregroundColor: foregroundColor]) - underlinedString.append(NSAttributedString(string: underlinePart, attributes: [.underlineStyle: NSUnderlineStyle.single.rawValue, .foregroundColor: underlineForegroundColor])) - underlinedString.append(NSAttributedString(string: lastPart, attributes: [.foregroundColor: foregroundColor])) - - return underlinedString - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/UIButton+Styles.swift b/Sources/WordPressAuthenticator/Extensions/UIButton+Styles.swift deleted file mode 100644 index cd05f518e53f..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/UIButton+Styles.swift +++ /dev/null @@ -1,16 +0,0 @@ -import UIKit -import WordPressShared - -extension UIButton { - /// Applies the style that looks like a plain text link. - func applyLinkButtonStyle() { - backgroundColor = .clear - titleLabel?.font = WPStyleGuide.fontForTextStyle(.body) - titleLabel?.textAlignment = .natural - - let buttonTitleColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonColor ?? WordPressAuthenticator.shared.style.textButtonColor - let buttonHighlightColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonHighlightColor ?? WordPressAuthenticator.shared.style.textButtonHighlightColor - setTitleColor(buttonTitleColor, for: .normal) - setTitleColor(buttonHighlightColor, for: .highlighted) - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/UIImage+Assets.swift b/Sources/WordPressAuthenticator/Extensions/UIImage+Assets.swift deleted file mode 100644 index be6e9b763a86..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/UIImage+Assets.swift +++ /dev/null @@ -1,44 +0,0 @@ -import UIKit - -// MARK: - Named Assets -// -extension UIImage { - /// Returns the Link Image. - /// - static var linkFieldImage: UIImage { - return UIImage(named: "icon-url-field", in: bundle, compatibleWith: nil) ?? UIImage() - } - - /// Returns the Default Magic Link Image. - /// - static var magicLinkImage: UIImage { - return UIImage(named: "login-magic-link", in: bundle, compatibleWith: nil) ?? UIImage() - } - - /// Returns the Link Image. - /// - @objc - public static var googleIcon: UIImage { - return UIImage(named: "google", in: bundle, compatibleWith: nil) ?? UIImage() - } - - /// Returns the Phone Icon. - /// - @objc - public static var phoneIcon: UIImage { - return UIImage(named: "phone-icon", in: bundle, compatibleWith: nil)?.withRenderingMode(.alwaysTemplate) ?? UIImage() - } - - /// Returns the Key Icon. - /// - @objc - public static var keyIcon: UIImage { - return UIImage(named: "key-icon", in: bundle, compatibleWith: nil)?.withRenderingMode(.alwaysTemplate) ?? UIImage() - } - - /// Returns WordPressAuthenticator's Bundle - /// - private static var bundle: Bundle { - return WordPressAuthenticator.bundle - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/UIPasteboard+Detect.swift b/Sources/WordPressAuthenticator/Extensions/UIPasteboard+Detect.swift deleted file mode 100644 index d0b3ff46a686..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/UIPasteboard+Detect.swift +++ /dev/null @@ -1,79 +0,0 @@ -import UIKit - -extension UIPasteboard { - - /// Detects patterns and values from the UIPasteboard. This will not trigger the pasteboard alert in iOS 14. - /// - Parameters: - /// - patterns: The patterns to detect. - /// - completion: Called with the patterns and values if any were detected, otherwise contains the errors from UIPasteboard. - @available(iOS 14.0, *) - func detect(patterns: Set, completion: @escaping (Result<[UIPasteboard.DetectionPattern: Any], Error>) -> Void) { - UIPasteboard.general.detectPatterns(for: patterns) { result in - switch result { - case .success(let detections): - guard detections.isEmpty == false else { - DispatchQueue.main.async { - completion(.success([UIPasteboard.DetectionPattern: Any]())) - } - return - } - UIPasteboard.general.detectValues(for: patterns) { valuesResult in - DispatchQueue.main.async { - completion(valuesResult) - } - } - case .failure(let error): - completion(.failure(error)) - } - } - } - - /// Attempts to detect and return a authenticator code from the pasteboard. - /// Expects to run on main thread. - /// - Parameters: - /// - completion: Called with a length digit authentication code on success - @available(iOS 14.0, *) - public func detectAuthenticatorCode(length: Int = 6, completion: @escaping (Result) -> Void) { - UIPasteboard.general.detect(patterns: [.number]) { result in - switch result { - case .success(let detections): - guard let firstMatch = detections.first else { - completion(.success("")) - return - } - guard let matchedNumber = firstMatch.value as? NSNumber else { - completion(.success("")) - return - } - - let authenticationCode = matchedNumber.stringValue - - /// Reject numbers with decimal points or signs in them - let codeCharacterSet = CharacterSet(charactersIn: authenticationCode) - if !codeCharacterSet.isSubset(of: CharacterSet.decimalDigits) { - completion(.success("")) - return - } - - /// We need length digits. No more, no less. - if authenticationCode.count > length { - completion(.success("")) - return - } else if authenticationCode.count == length { - completion(.success(authenticationCode)) - return - } - - let missingDigits = 6 - authenticationCode.count - let paddingZeros = String(repeating: "0", count: missingDigits) - let paddedAuthenticationCode = paddingZeros + authenticationCode - - completion(.success(paddedAuthenticationCode)) - return - case .failure(let error): - completion(.failure(error)) - return - } - } - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/UIStoryboard+Helpers.swift b/Sources/WordPressAuthenticator/Extensions/UIStoryboard+Helpers.swift deleted file mode 100644 index 796ab269c7c6..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/UIStoryboard+Helpers.swift +++ /dev/null @@ -1,30 +0,0 @@ -import UIKit - -// MARK: - Storyboard enum -enum Storyboard: String { - case login = "Login" - case signup = "Signup" - case getStarted = "GetStarted" - case unifiedSignup = "UnifiedSignup" - case unifiedLoginMagicLink = "LoginMagicLink" - case emailMagicLink = "EmailMagicLink" - case siteAddress = "SiteAddress" - case googleAuth = "GoogleAuth" - case googleSignupConfirmation = "GoogleSignupConfirmation" - case twoFA = "TwoFA" - case password = "Password" - case verifyEmail = "VerifyEmail" - case nuxButtonView = "NUXButtonView" - - var instance: UIStoryboard { - return UIStoryboard(name: self.rawValue, bundle: WordPressAuthenticator.bundle) - } - - /// Returns a view controller from a Storyboard - /// assuming the identifier is the same as the class name. - /// - func instantiateViewController(ofClass classType: T.Type, creator: ((NSCoder) -> UIViewController?)? = nil) -> T? { - let identifier = classType.classNameWithoutNamespaces - return instance.instantiateViewController(identifier: identifier, creator: creator) as? T - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/UITableView+Helpers.swift b/Sources/WordPressAuthenticator/Extensions/UITableView+Helpers.swift deleted file mode 100644 index 972468aa3140..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/UITableView+Helpers.swift +++ /dev/null @@ -1,20 +0,0 @@ -import UIKit - -extension UITableView { - /// Called in view controller's `viewDidLayoutSubviews`. If table view has a footer view, calculates the new height. - /// If new height is different from current height, updates the footer view with the new height and reassigns the table footer view. - /// Note: make sure the top-level footer view (`tableView.tableFooterView`) is frame based as a container of the Auto Layout based subview. - func updateFooterHeight() { - if let footerView = tableFooterView { - let targetSize = CGSize(width: footerView.frame.width, height: 0) - let newSize = footerView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow) - let newHeight = newSize.height - var currentFrame = footerView.frame - if newHeight != currentFrame.size.height { - currentFrame.size.height = newHeight - footerView.frame = currentFrame - tableFooterView = footerView - } - } - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/UIView+AuthHelpers.swift b/Sources/WordPressAuthenticator/Extensions/UIView+AuthHelpers.swift deleted file mode 100644 index 36e29efa3cbe..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/UIView+AuthHelpers.swift +++ /dev/null @@ -1,18 +0,0 @@ -import UIKit - -/// UIView class methods -/// -extension UIView { - /// Returns the Nib associated with the received: It's filename is expected to match the Class Name - /// - class func loadNib() -> UINib { - return UINib(nibName: classNameWithoutNamespaces, bundle: WordPressAuthenticator.bundle) - } - - /// Returns the first Object contained within the nib with a name whose name matches with the receiver's type. - /// Note: On error this method is expected to break, by design! - /// - class func instantiateFromNib() -> T { - return loadNib().instantiate(withOwner: nil, options: nil).first as! T - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/UIViewController+Dismissal.swift b/Sources/WordPressAuthenticator/Extensions/UIViewController+Dismissal.swift deleted file mode 100644 index b1d721a9d119..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/UIViewController+Dismissal.swift +++ /dev/null @@ -1,22 +0,0 @@ -import UIKit - -extension UIViewController { - - /// Depending on how a VC is presented we need to check different things to know whether it's being dismissed or not. - /// A VC presented as the first VC in a navigation controller needs to check if the navigation controller is being dismissed. - /// A VC added to an existing navigation controller is dismissed when `isMovingFromParent` is `true`. - /// For any other scenario `isBeingDismissed` will do. - /// - var isBeingDismissedInAnyWay: Bool { - isMovingFromParent || isBeingDismissed || (navigationController?.isBeingDismissed ?? false) - } - - /// Depending on how a VC is presented we need to check different things to know whether it's being presented or not. - /// A VC presented as the first VC in a navigation controller needs to check if the navigation controller is being presented. - /// A VC added to an existing navigation controller is presented when `isMovingToParent` is `true`. - /// For any other scenario `isBeingPresented` will do. - /// - var isBeingPresentedInAnyWay: Bool { - isMovingToParent || isBeingPresented || (navigationController?.isBeingPresented ?? false) - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/UIViewController+Helpers.swift b/Sources/WordPressAuthenticator/Extensions/UIViewController+Helpers.swift deleted file mode 100644 index c35a378e910d..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/UIViewController+Helpers.swift +++ /dev/null @@ -1,11 +0,0 @@ -import UIKit - -// MARK: - UIViewController Helpers -extension UIViewController { - - /// Convenience method to instantiate a view controller from a storyboard. - /// - static func instantiate(from storyboard: Storyboard, creator: ((NSCoder) -> UIViewController?)? = nil) -> Self? { - return storyboard.instantiateViewController(ofClass: self, creator: creator) - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/URL+JetpackConnect.swift b/Sources/WordPressAuthenticator/Extensions/URL+JetpackConnect.swift deleted file mode 100644 index 230d3616d603..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/URL+JetpackConnect.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -extension URL { - public var isJetpackConnect: Bool { - query?.contains("&source=jetpack") ?? false - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/WPStyleGuide+Login.swift b/Sources/WordPressAuthenticator/Extensions/WPStyleGuide+Login.swift deleted file mode 100644 index 4326e870ff30..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/WPStyleGuide+Login.swift +++ /dev/null @@ -1,343 +0,0 @@ -import UIKit -import WordPressShared -import WordPressUI -import Gridicons -import AuthenticationServices - -final class SubheadlineButton: UIButton { - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { - titleLabel?.font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - setTitleColor(WordPressAuthenticator.shared.style.textButtonColor, for: .normal) - setTitleColor(WordPressAuthenticator.shared.style.textButtonHighlightColor, for: .highlighted) - } - } -} - -extension WPStyleGuide { - - private struct Constants { - static let textButtonMinHeight: CGFloat = 40.0 - static let googleIconOffset: CGFloat = -1.0 - static let googleIconButtonSize: CGFloat = 15.0 - static let domainsIconPaddingToRemove: CGFloat = 2.0 - static let domainsIconSize = CGSize(width: 18, height: 18) - static let verticalLabelSpacing: CGFloat = 10.0 - } - - /// Calculate the border based on the display - /// - class var hairlineBorderWidth: CGFloat { - return 1.0 / UIScreen.main.scale - } - - /// Common view style for signin view controllers. - /// - /// - Parameters: - /// - view: The view to style. - /// - class func configureColorsForSigninView(_ view: UIView) { - view.backgroundColor = wordPressBlue() - } - - /// Configures a plain text button with default styles. - /// - class func configureTextButton(_ button: UIButton) { - button.setTitleColor(WordPressAuthenticator.shared.style.textButtonColor, for: .normal) - button.setTitleColor(WordPressAuthenticator.shared.style.textButtonHighlightColor, for: .highlighted) - } - - /// - class func edgeInsetForLoginTextFields() -> UIEdgeInsets { - return UIEdgeInsets(top: 7, left: 20, bottom: 7, right: 20) - } - - class func textInsetsForLoginTextFieldWithLeftView() -> UIEdgeInsets { - return UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0) - } - - /// Return the system font in medium weight for the given style - /// - /// - note: iOS won't return UIFontWeightMedium for dynamic system font :( - /// So instead get the dynamic font size, then ask for the non-dynamic font at that size - /// - class func mediumWeightFont(forStyle style: UIFont.TextStyle, maximumPointSize: CGFloat = WPStyleGuide.maxFontSize) -> UIFont { - let fontToGetSize = WPStyleGuide.fontForTextStyle(style) - let maxAllowedFontSize = CGFloat.minimum(fontToGetSize.pointSize, maximumPointSize) - return UIFont.systemFont(ofSize: maxAllowedFontSize, weight: .medium) - } - - // MARK: - Login Button Methods - - /// Creates a button for Google Sign-in with hyperlink style. - /// - /// - Returns: A properly styled UIButton - /// - class func googleLoginButton() -> UIButton { - let baseString = NSLocalizedString("{G} Log in with Google.", comment: "Label for button to log in using Google. The {G} will be replaced with the Google logo.") - - let attrStrNormal = googleButtonString(baseString, linkColor: WordPressAuthenticator.shared.style.textButtonColor) - let attrStrHighlight = googleButtonString(baseString, linkColor: WordPressAuthenticator.shared.style.textButtonHighlightColor) - - let font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - - return textButton(normal: attrStrNormal, highlighted: attrStrHighlight, font: font) - } - - /// Creates an attributed string that includes the Google logo. - /// - /// - Parameters: - /// - forHyperlink: Indicates if the string will be displayed in a hyperlink. - /// Otherwise, it will be styled to be displayed on a NUXButton. - /// - Returns: A properly styled NSAttributedString - /// - class func formattedGoogleString(forHyperlink: Bool = false) -> NSAttributedString { - - let googleAttachment = NSTextAttachment() - let googleIcon = UIImage.googleIcon - googleAttachment.image = googleIcon - - if forHyperlink { - // Create an attributed string that contains the Google icon. - let font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - googleAttachment.bounds = CGRect(x: 0, - y: font.descender + Constants.googleIconOffset, - width: googleIcon.size.width, - height: googleIcon.size.height) - - return NSAttributedString(attachment: googleAttachment) - } else { - let nuxButtonTitleFont = WPStyleGuide.mediumWeightFont(forStyle: .title3) - let googleTitle = NSLocalizedString("Continue with Google", - comment: "Button title. Tapping begins log in using Google.") - return attributedStringwithLogo(googleIcon, - imageSize: .init(width: Constants.googleIconButtonSize, height: Constants.googleIconButtonSize), - title: googleTitle, - titleFont: nuxButtonTitleFont) - } - } - - /// Creates an attributed string that includes the Apple logo. - /// - /// - Returns: A properly styled NSAttributedString to be displayed on a NUXButton. - /// - class func formattedAppleString() -> NSAttributedString { - let attributedString = NSMutableAttributedString() - - let appleSymbol = "" - let appleSymbolAttributes: [NSAttributedString.Key: Any] = [ - .font: UIFont.systemFont(ofSize: 23) - ] - attributedString.append(NSAttributedString(string: appleSymbol, attributes: appleSymbolAttributes)) - - // Add leading non-breaking space to separate the button text from the Apple symbol. - let space = "\u{00a0}\u{00a0}" - attributedString.append(NSAttributedString(string: space)) - - let title = NSLocalizedString("Continue with Apple", comment: "Button title. Tapping begins log in using Apple.") - attributedString.append(NSAttributedString(string: title)) - - return NSAttributedString(attributedString: attributedString) - } - - /// Creates an attributed string that includes the `linkFieldImage` - /// - /// - Returns: A properly styled NSAttributedString to be displayed on a NUXButton. - /// - class func formattedSignInWithSiteCredentialsString() -> NSAttributedString { - let title = WordPressAuthenticator.shared.displayStrings.signInWithSiteCredentialsButtonTitle - let globe = UIImage.gridicon(.globe) - let image = globe.imageWithTintColor(WordPressAuthenticator.shared.style.placeholderColor) ?? globe - return attributedStringwithLogo(image, - imageSize: image.size, - title: title, - titleFont: WPStyleGuide.mediumWeightFont(forStyle: .title3)) - } - - /// Creates a button for Self-hosted Login - /// - /// - Returns: A properly styled UIButton - /// - class func selfHostedLoginButton(alignment: UIControl.NaturalContentHorizontalAlignment = .leading) -> UIButton { - - let style = WordPressAuthenticator.shared.style - - let button: UIButton - - if WordPressAuthenticator.shared.configuration.showLoginOptions { - let baseString = NSLocalizedString("Or log in by _entering your site address_.", comment: "Label for button to log in using site address. Underscores _..._ denote underline.") - - let attrStrNormal = baseString.underlined(color: style.subheadlineColor, underlineColor: style.textButtonColor) - let attrStrHighlight = baseString.underlined(color: style.subheadlineColor, underlineColor: style.textButtonHighlightColor) - let font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - - button = textButton(normal: attrStrNormal, highlighted: attrStrHighlight, font: font, alignment: alignment) - } else { - let baseString = NSLocalizedString("Enter the address of the WordPress site you'd like to connect.", comment: "Label for button to log in using your site address.") - - let attrStrNormal = selfHostedButtonString(baseString, linkColor: style.textButtonColor) - let attrStrHighlight = selfHostedButtonString(baseString, linkColor: style.textButtonHighlightColor) - let font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - - button = textButton(normal: attrStrNormal, highlighted: attrStrHighlight, font: font) - } - - button.accessibilityIdentifier = "Self Hosted Login Button" - - return button - } - - /// Creates a button for wpcom signup on the email screen - /// - /// - Returns: A UIButton styled for wpcom signup - /// - Note: This button is only used during Jetpack setup, not the usual flows - /// - class func wpcomSignupButton() -> UIButton { - let style = WordPressAuthenticator.shared.style - let baseString = NSLocalizedString("Don't have an account? _Sign up_", comment: "Label for button to log in using your site address. The underscores _..._ denote underline") - let attrStrNormal = baseString.underlined(color: style.subheadlineColor, underlineColor: style.textButtonColor) - let attrStrHighlight = baseString.underlined(color: style.subheadlineColor, underlineColor: style.textButtonHighlightColor) - let font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - - return textButton(normal: attrStrNormal, highlighted: attrStrHighlight, font: font) - } - - /// Creates a button to open our T&C - /// - /// - Returns: A properly styled UIButton - /// - class func termsButton() -> UIButton { - let style = WordPressAuthenticator.shared.style - - let baseString = NSLocalizedString("By signing up, you agree to our _Terms of Service_.", comment: "Legal disclaimer for signup buttons, the underscores _..._ denote underline") - - let attrStrNormal = baseString.underlined(color: style.subheadlineColor, underlineColor: style.textButtonColor) - let attrStrHighlight = baseString.underlined(color: style.subheadlineColor, underlineColor: style.textButtonHighlightColor) - let font = WPStyleGuide.mediumWeightFont(forStyle: .footnote) - - return textButton(normal: attrStrNormal, highlighted: attrStrHighlight, font: font, alignment: .center) - } - - /// Creates a button to open our T&C. - /// Specifically, the Sign Up verbiage on the Get Started view. - /// - Returns: A properly styled UIButton - /// - class func signupTermsButton() -> UIButton { - let unifiedStyle = WordPressAuthenticator.shared.unifiedStyle - let originalStyle = WordPressAuthenticator.shared.style - let baseString = WordPressAuthenticator.shared.displayStrings.signupTermsOfService - let textColor = unifiedStyle?.textSubtleColor ?? originalStyle.subheadlineColor - let linkColor = unifiedStyle?.textButtonColor ?? originalStyle.textButtonColor - - let attrStrNormal = baseString.underlined(color: textColor, underlineColor: linkColor) - let attrStrHighlight = baseString.underlined(color: textColor, underlineColor: linkColor) - let font = WPStyleGuide.mediumWeightFont(forStyle: .footnote) - - let button = textButton(normal: attrStrNormal, highlighted: attrStrHighlight, font: font, alignment: .center, forUnified: true) - button.titleLabel?.textAlignment = .center - return button - } - - private class func textButton(normal normalString: NSAttributedString, highlighted highlightString: NSAttributedString, font: UIFont, alignment: UIControl.NaturalContentHorizontalAlignment = .leading, forUnified: Bool = false) -> UIButton { - let button = SubheadlineButton() - button.clipsToBounds = true - - button.naturalContentHorizontalAlignment = alignment - button.translatesAutoresizingMaskIntoConstraints = false - button.titleLabel?.font = font - button.titleLabel?.numberOfLines = 0 - button.titleLabel?.lineBreakMode = .byWordWrapping - button.setTitleColor(WordPressAuthenticator.shared.style.subheadlineColor, for: .normal) - - // These constraints work around some issues with multiline buttons and - // vertical layout. Without them the button's height may not account - // for the titleLabel's height. - - let verticalLabelSpacing = forUnified ? 0 : Constants.verticalLabelSpacing - button.titleLabel?.topAnchor.constraint(equalTo: button.topAnchor, constant: verticalLabelSpacing).isActive = true - button.titleLabel?.bottomAnchor.constraint(equalTo: button.bottomAnchor, constant: -verticalLabelSpacing).isActive = true - button.heightAnchor.constraint(greaterThanOrEqualToConstant: Constants.textButtonMinHeight).isActive = true - - button.setAttributedTitle(normalString, for: .normal) - button.setAttributedTitle(highlightString, for: .highlighted) - return button - } - - private class func googleButtonString(_ baseString: String, linkColor: UIColor) -> NSAttributedString { - let labelParts = baseString.components(separatedBy: "{G}") - - let firstPart = labelParts[0] - // 👇 don't want to crash when a translation lacks "{G}" - let lastPart = labelParts.indices.contains(1) ? labelParts[1] : "" - - let labelString = NSMutableAttributedString(string: firstPart, attributes: [.foregroundColor: WPStyleGuide.greyDarken30()]) - - if !lastPart.isEmpty { - labelString.append(formattedGoogleString(forHyperlink: true)) - } - - labelString.append(NSAttributedString(string: lastPart, attributes: [.foregroundColor: linkColor])) - - return labelString - } - - private class func selfHostedButtonString(_ buttonText: String, linkColor: UIColor) -> NSAttributedString { - let font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - - let titleParagraphStyle = NSMutableParagraphStyle() - titleParagraphStyle.alignment = .left - - let labelString = NSMutableAttributedString(string: "") - - if let originalDomainsIcon = UIImage.gridicon(.domains).imageWithTintColor(WordPressAuthenticator.shared.style.placeholderColor) { - var domainsIcon = originalDomainsIcon.cropping(to: CGRect(x: Constants.domainsIconPaddingToRemove, - y: Constants.domainsIconPaddingToRemove, - width: originalDomainsIcon.size.width - Constants.domainsIconPaddingToRemove * 2, - height: originalDomainsIcon.size.height - Constants.domainsIconPaddingToRemove * 2)) - domainsIcon = domainsIcon.resized(to: Constants.domainsIconSize) - let domainsAttachment = NSTextAttachment() - domainsAttachment.image = domainsIcon - domainsAttachment.bounds = CGRect(x: 0, y: font.descender, width: domainsIcon.size.width, height: domainsIcon.size.height) - let iconString = NSAttributedString(attachment: domainsAttachment) - labelString.append(iconString) - } - labelString.append(NSAttributedString(string: " " + buttonText, attributes: [.foregroundColor: linkColor])) - - return labelString - } -} - -// MARK: Attributed String Helpers -// -private extension WPStyleGuide { - - /// Creates an attributed string with a logo and title. - /// The logo is prepended to the title. - /// - /// - Parameters: - /// - logoImage: UIImage representing the logo - /// - imageSize: Size of the UIImage - /// - title: title String to be appended to the logoImage - /// - titleFont: UIFont for the title String - /// - /// - Returns: A properly styled NSAttributedString to be displayed on a NUXButton. - /// - class func attributedStringwithLogo(_ logoImage: UIImage, - imageSize: CGSize, - title: String, - titleFont: UIFont) -> NSAttributedString { - let attachment = NSTextAttachment() - attachment.image = logoImage - - attachment.bounds = CGRect(x: 0, y: (titleFont.capHeight - imageSize.height) / 2, - width: imageSize.width, height: imageSize.height) - - let buttonString = NSMutableAttributedString(attachment: attachment) - // Add leading non-breaking spaces to separate the button text from the logo. - let title = "\u{00a0}\u{00a0}" + title - buttonString.append(NSAttributedString(string: title)) - - return buttonString - } -} diff --git a/Sources/WordPressAuthenticator/Features/EmailClientPicker/AppSelector.swift b/Sources/WordPressAuthenticator/Features/EmailClientPicker/AppSelector.swift deleted file mode 100644 index 24c755e2453c..000000000000 --- a/Sources/WordPressAuthenticator/Features/EmailClientPicker/AppSelector.swift +++ /dev/null @@ -1,130 +0,0 @@ -import UIKit -import MessageUI - -/// App selector that selects an app from a list and opens it -/// Note: it's a wrapper of UIAlertController (which cannot be sublcassed) -public class AppSelector { - // the action sheet that will contain the list of apps that can be called - let alertController: UIAlertController - - /// initializes the picker with a dictionary. Initialization will fail if an empty/invalid app list is passed - /// - Parameters: - /// - appList: collection of apps to be added to the selector - /// - defaultAction: default action, if not nil, will be the first element of the list - /// - sourceView: the sourceView to anchor the action sheet to - /// - urlHandler: object that handles app URL schemes; defaults to UIApplication.shared - public init?(with appList: [String: String], - defaultAction: UIAlertAction? = nil, - sourceView: UIView, - urlHandler: URLHandler = UIApplication.shared) { - /// inline method that builds a list of app calls to be inserted in the action sheet - func makeAlertActions(from appList: [String: String]) -> [UIAlertAction]? { - guard !appList.isEmpty else { - return nil - } - - var actions = [UIAlertAction]() - for (name, urlString) in appList { - guard let url = URL(string: urlString), urlHandler.canOpenURL(url) else { - continue - } - actions.append(UIAlertAction(title: AppSelectorTitles(rawValue: name)?.localized ?? name, style: .default) { _ in - urlHandler.open(url, options: [:], completionHandler: nil) - }) - } - - guard !actions.isEmpty else { - return nil - } - // sort the apps alphabetically - actions = actions.sorted { $0.title ?? "" < $1.title ?? "" } - actions.append(UIAlertAction(title: AppSelectorTitles.cancel.localized, style: .cancel, handler: nil)) - - if let action = defaultAction { - actions.insert(action, at: 0) - } - return actions - } - - guard let appCalls = makeAlertActions(from: appList) else { - return nil - } - - alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - alertController.popoverPresentationController?.sourceView = sourceView - alertController.popoverPresentationController?.sourceRect = sourceView.bounds - appCalls.forEach { - alertController.addAction($0) - } - } -} - -/// Initializers for Email Picker -public extension AppSelector { - /// initializes the picker with a plist file in a specified bundle - convenience init?(with plistFile: String, - in bundle: Bundle, - defaultAction: UIAlertAction? = nil, - sourceView: UIView) { - - guard let plistPath = bundle.path(forResource: plistFile, ofType: "plist"), - let availableApps = NSDictionary(contentsOfFile: plistPath) as? [String: String] else { - return nil - } - self.init(with: availableApps, - defaultAction: defaultAction, - sourceView: sourceView) - } - - /// Convenience init for a picker that calls supported email clients apps, defined in EmailClients.plist - convenience init?(sourceView: UIView) { - let wpAuthenticatorBundle = WordPressAuthenticator.bundle - - let plistFile = "EmailClients" - var defaultAction: UIAlertAction? - - // if available, prepend apple mail - if MFMailComposeViewController.canSendMail(), let url = URL(string: "message://") { - defaultAction = UIAlertAction(title: AppSelectorTitles.appleMail.localized, style: .default) { _ in - UIApplication.shared.open(url) - } - } - self.init(with: plistFile, - in: wpAuthenticatorBundle, - defaultAction: defaultAction, - sourceView: sourceView) - } -} - -/// Localizable app selector titles -enum AppSelectorTitles: String { - case appleMail - case gmail - case airmail - case msOutlook - case spark - case yahooMail - case fastmail - case cancel - - var localized: String { - switch self { - case .appleMail: - return NSLocalizedString("Mail (Default)", comment: "Option to select the Apple Mail app when logging in with magic links") - case .gmail: - return NSLocalizedString("Gmail", comment: "Option to select the Gmail app when logging in with magic links") - case .airmail: - return NSLocalizedString("Airmail", comment: "Option to select the Airmail app when logging in with magic links") - case .msOutlook: - return NSLocalizedString("Microsoft Outlook", comment: "Option to select the Microsft Outlook app when logging in with magic links") - case .spark: - return NSLocalizedString("Spark", comment: "Option to select the Spark email app when logging in with magic links") - case .yahooMail: - return NSLocalizedString("Yahoo Mail", comment: "Option to select the Yahoo Mail app when logging in with magic links") - case .fastmail: - return NSLocalizedString("Fastmail", comment: "Option to select the Fastmail app when logging in with magic links") - case .cancel: - return NSLocalizedString("Cancel", comment: "Option to cancel the email app selection when logging in with magic links") - } - } -} diff --git a/Sources/WordPressAuthenticator/Features/EmailClientPicker/LinkMailPresenter.swift b/Sources/WordPressAuthenticator/Features/EmailClientPicker/LinkMailPresenter.swift deleted file mode 100644 index 7e12c0b3317b..000000000000 --- a/Sources/WordPressAuthenticator/Features/EmailClientPicker/LinkMailPresenter.swift +++ /dev/null @@ -1,48 +0,0 @@ -import MessageUI - -/// Email picker presenter -public class LinkMailPresenter { - - private let emailAddress: String - - public init(emailAddress: String) { - self.emailAddress = emailAddress - } - - /// Presents the available mail clients in an action sheet. If none is available, - /// Falls back to Apple Mail and opens it. - /// If not even Apple Mail is available, presents an alert to check your email - /// - Parameters: - /// - viewController: the UIViewController that will present the action sheet - /// - appSelector: the app picker that contains the available clients. Nil if no clients are available - /// reads the supported email clients from EmailClients.plist - public func presentEmailClients(on viewController: UIViewController, - appSelector: AppSelector?) { - - guard let picker = appSelector else { - // fall back to Apple Mail if no other clients are installed - if MFMailComposeViewController.canSendMail(), let url = URL(string: "message://") { - UIApplication.shared.open(url) - } else { - showAlertToCheckEmail(on: viewController) - } - return - } - viewController.present(picker.alertController, animated: true) - } - - private func showAlertToCheckEmail(on viewController: UIViewController) { - let title = NSLocalizedString("Check your email!", - comment: "Alert title for check your email during logIn/signUp.") - - let message = String.localizedStringWithFormat(NSLocalizedString("We just emailed a link to %@. Please check your mail app and tap the link to log in.", - comment: "message to ask a user to check their email for a WordPress.com email"), emailAddress) - - let alertController = UIAlertController(title: title, - message: message, - preferredStyle: .alert) - alertController.addCancelActionWithTitle(NSLocalizedString("OK", - comment: "Button title. An acknowledgement of the message displayed in a prompt.")) - viewController.present(alertController, animated: true, completion: nil) - } -} diff --git a/Sources/WordPressAuthenticator/Features/EmailClientPicker/URLHandler.swift b/Sources/WordPressAuthenticator/Features/EmailClientPicker/URLHandler.swift deleted file mode 100644 index dfe18e973192..000000000000 --- a/Sources/WordPressAuthenticator/Features/EmailClientPicker/URLHandler.swift +++ /dev/null @@ -1,15 +0,0 @@ -import UIKit - -/// Generic type that handles URL Schemes -public protocol URLHandler { - /// checks if the specified URL can be opened - func canOpenURL(_ url: URL) -> Bool - - /// opens the specified URL - func open(_ url: URL, - options: [UIApplication.OpenExternalURLOptionsKey: Any], - completionHandler completion: (@MainActor @Sendable (Bool) -> Void)?) -} - -/// conforms UIApplication to URLHandler to allow dependency injection -extension UIApplication: URLHandler {} diff --git a/Sources/WordPressAuthenticator/Features/NUX/Button/NUXButton.swift b/Sources/WordPressAuthenticator/Features/NUX/Button/NUXButton.swift deleted file mode 100644 index bc1a292725a4..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/Button/NUXButton.swift +++ /dev/null @@ -1,266 +0,0 @@ -import UIKit -import WordPressShared -import WordPressKit - -public struct NUXButtonStyle { - public let normal: ButtonStyle - public let highlighted: ButtonStyle - public let disabled: ButtonStyle - - public struct ButtonStyle { - public let backgroundColor: UIColor - public let borderColor: UIColor - public let titleColor: UIColor - - public init(backgroundColor: UIColor, borderColor: UIColor, titleColor: UIColor) { - self.backgroundColor = backgroundColor - self.borderColor = borderColor - self.titleColor = titleColor - } - } - - public init(normal: ButtonStyle, highlighted: ButtonStyle, disabled: ButtonStyle) { - self.normal = normal - self.highlighted = highlighted - self.disabled = disabled - } - - public static var linkButtonStyle: NUXButtonStyle { - let backgroundColor = UIColor.clear - let buttonTitleColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonColor ?? WordPressAuthenticator.shared.style.textButtonColor - let buttonHighlightColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonHighlightColor ?? WordPressAuthenticator.shared.style.textButtonHighlightColor - - let normalButtonStyle = ButtonStyle(backgroundColor: backgroundColor, - borderColor: backgroundColor, - titleColor: buttonTitleColor) - let highlightedButtonStyle = ButtonStyle(backgroundColor: backgroundColor, - borderColor: backgroundColor, - titleColor: buttonHighlightColor) - let disabledButtonStyle = ButtonStyle(backgroundColor: backgroundColor, - borderColor: backgroundColor, - titleColor: buttonTitleColor.withAlphaComponent(0.5)) - return NUXButtonStyle(normal: normalButtonStyle, - highlighted: highlightedButtonStyle, - disabled: disabledButtonStyle) - } -} -/// A stylized button used by Login controllers. It also can display a `UIActivityIndicatorView`. -@objc open class NUXButton: UIButton { - @objc var isAnimating: Bool { - return activityIndicator.isAnimating - } - - var buttonStyle: NUXButtonStyle? - - open override var isEnabled: Bool { - didSet { - activityIndicator.color = activityIndicatorColor(isEnabled: isEnabled) - } - } - - @objc let activityIndicator: UIActivityIndicatorView = { - let indicator = UIActivityIndicatorView(style: .medium) - indicator.hidesWhenStopped = true - return indicator - }() - - var titleFont = WPStyleGuide.mediumWeightFont(forStyle: .title3) - - override open func layoutSubviews() { - super.layoutSubviews() - - if activityIndicator.isAnimating { - titleLabel?.frame = CGRect.zero - - var frm = activityIndicator.frame - frm.origin.x = (frame.width - frm.width) / 2.0 - frm.origin.y = (frame.height - frm.height) / 2.0 - activityIndicator.frame = frm.integral - } - } - - open override func tintColorDidChange() { - // Update colors when toggling light/dark mode. - super.tintColorDidChange() - configureBackgrounds() - configureTitleColors() - - if socialService == .apple { - setAttributedTitle(WPStyleGuide.formattedAppleString(), for: .normal) - } - } - - // MARK: - Instance Methods - - /// Toggles the visibility of the activity indicator. When visible the button - /// title is hidden. - /// - /// - Parameter show: True to show the spinner. False hides it. - /// - open func showActivityIndicator(_ show: Bool) { - if show { - activityIndicator.startAnimating() - } else { - activityIndicator.stopAnimating() - } - setNeedsLayout() - } - - func didChangePreferredContentSize() { - titleLabel?.adjustsFontForContentSizeCategory = true - } - - func customizeFont(_ font: UIFont) { - titleFont = font - } - - /// Indicates if the current instance should be rendered with the "Primary" Style. - /// - @IBInspectable public var isPrimary: Bool = false { - didSet { - configureBackgrounds() - configureTitleColors() - } - } - - var socialService: SocialServiceName? - - // MARK: - LifeCycle Methods - - open override func didMoveToWindow() { - super.didMoveToWindow() - configureAppearance() - } - - open override func awakeFromNib() { - super.awakeFromNib() - configureAppearance() - } - - /// Setup: Everything = [Insets, Backgrounds, titleColor(s), titleLabel] - /// - private func configureAppearance() { - configureInsets() - configureBackgrounds() - configureActivityIndicator() - configureTitleColors() - configureTitleLabel() - } - - /// Setup: NUXButton's Default Settings - /// - private func configureInsets() { - contentEdgeInsets = UIImage.DefaultRenderMetrics.contentInsets - } - - /// Setup: ActivityIndicator - /// - private func configureActivityIndicator() { - activityIndicator.color = activityIndicatorColor() - addSubview(activityIndicator) - } - - /// Setup: BackgroundImage - /// - private func configureBackgrounds() { - guard let buttonStyle else { - legacyConfigureBackgrounds() - return - } - - let normalImage = UIImage.renderBackgroundImage(fill: buttonStyle.normal.backgroundColor, - border: buttonStyle.normal.borderColor) - - let highlightedImage = UIImage.renderBackgroundImage(fill: buttonStyle.highlighted.backgroundColor, - border: buttonStyle.highlighted.borderColor) - - let disabledImage = UIImage.renderBackgroundImage(fill: buttonStyle.disabled.backgroundColor, - border: buttonStyle.disabled.borderColor) - - setBackgroundImage(normalImage, for: .normal) - setBackgroundImage(highlightedImage, for: .highlighted) - setBackgroundImage(disabledImage, for: .disabled) - } - - /// Fallback method to configure the background colors based on the shared `WordPressAuthenticatorStyle` - /// - private func legacyConfigureBackgrounds() { - let style = WordPressAuthenticator.shared.style - - let normalImage: UIImage - let highlightedImage: UIImage - let disabledImage = UIImage.renderBackgroundImage(fill: style.disabledBackgroundColor, - border: style.disabledBorderColor) - - if isPrimary { - normalImage = UIImage.renderBackgroundImage(fill: style.primaryNormalBackgroundColor, - border: style.primaryNormalBorderColor) - highlightedImage = UIImage.renderBackgroundImage(fill: style.primaryHighlightBackgroundColor, - border: style.primaryHighlightBorderColor) - } else { - normalImage = UIImage.renderBackgroundImage(fill: style.secondaryNormalBackgroundColor, - border: style.secondaryNormalBorderColor) - highlightedImage = UIImage.renderBackgroundImage(fill: style.secondaryHighlightBackgroundColor, - border: style.secondaryHighlightBorderColor) - } - - setBackgroundImage(normalImage, for: .normal) - setBackgroundImage(highlightedImage, for: .highlighted) - setBackgroundImage(disabledImage, for: .disabled) - } - - /// Setup: TitleColor - /// - private func configureTitleColors() { - guard let buttonStyle else { - legacyConfigureTitleColors() - return - } - - setTitleColor(buttonStyle.normal.titleColor, for: .normal) - setTitleColor(buttonStyle.highlighted.titleColor, for: .highlighted) - setTitleColor(buttonStyle.disabled.titleColor, for: .disabled) - } - - /// Fallback method to configure the title colors based on the shared `WordPressAuthenticatorStyle` - /// - private func legacyConfigureTitleColors() { - let style = WordPressAuthenticator.shared.style - let titleColorNormal = isPrimary ? style.primaryTitleColor : style.secondaryTitleColor - - setTitleColor(titleColorNormal, for: .normal) - setTitleColor(titleColorNormal, for: .highlighted) - setTitleColor(style.disabledTitleColor, for: .disabled) - } - - /// Setup: TitleLabel - /// - private func configureTitleLabel() { - titleLabel?.font = self.titleFont - titleLabel?.adjustsFontForContentSizeCategory = true - titleLabel?.textAlignment = .center - } - - /// Returns the current color that should be used for the activity indicator - /// - private func activityIndicatorColor(isEnabled: Bool = true) -> UIColor { - guard let style = buttonStyle else { - let style = WordPressAuthenticator.shared.style - - return isEnabled ? style.primaryTitleColor : style.disabledButtonActivityIndicatorColor - } - - return isEnabled ? style.normal.titleColor : style.disabled.titleColor - } -} - -// MARK: - -// -extension NUXButton { - override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { - didChangePreferredContentSize() - } - } -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/Button/NUXButtonView.storyboard b/Sources/WordPressAuthenticator/Features/NUX/Button/NUXButtonView.storyboard deleted file mode 100644 index 31db7e8cf3c6..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/Button/NUXButtonView.storyboard +++ /dev/null @@ -1,206 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Features/NUX/Button/NUXButtonViewController.swift b/Sources/WordPressAuthenticator/Features/NUX/Button/NUXButtonViewController.swift deleted file mode 100644 index bf5e2457049d..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/Button/NUXButtonViewController.swift +++ /dev/null @@ -1,315 +0,0 @@ -import UIKit -import WordPressKit -import WordPressShared - -public protocol NUXButtonViewControllerDelegate: AnyObject { - func primaryButtonPressed() - func secondaryButtonPressed() - func tertiaryButtonPressed() -} - -extension NUXButtonViewControllerDelegate { - public func secondaryButtonPressed() {} - public func tertiaryButtonPressed() {} -} - -struct NUXButtonConfig { - typealias CallBackType = () -> Void - - let title: String? - let attributedTitle: NSAttributedString? - let socialService: SocialServiceName? - let isPrimary: Bool - let configureBodyFontForTitle: Bool? - let accessibilityIdentifier: String? - let callback: CallBackType? - - init(title: String? = nil, attributedTitle: NSAttributedString? = nil, socialService: SocialServiceName? = nil, isPrimary: Bool, configureBodyFontForTitle: Bool? = nil, accessibilityIdentifier: String? = nil, callback: CallBackType?) { - self.title = title - self.attributedTitle = attributedTitle - self.socialService = socialService - self.isPrimary = isPrimary - self.configureBodyFontForTitle = configureBodyFontForTitle - self.accessibilityIdentifier = accessibilityIdentifier - self.callback = callback - } -} - -open class NUXButtonViewController: UIViewController { - typealias CallBackType = () -> Void - - // MARK: - Properties - - @IBOutlet var stackView: UIStackView? - @IBOutlet var bottomButton: NUXButton? - @IBOutlet var topButton: NUXButton? - @IBOutlet var tertiaryButton: NUXButton? - @IBOutlet var buttonHolder: UIView? - - @IBOutlet private var shadowView: UIImageView? - @IBOutlet private var shadowViewEdgeConstraints: [NSLayoutConstraint]! - - /// Used to constrain the shadow view outside of the - /// bounds of this view controller. - var shadowLayoutGuide: UILayoutGuide? { - didSet { - updateShadowViewEdgeConstraints() - } - } - - open weak var delegate: NUXButtonViewControllerDelegate? - open var backgroundColor: UIColor? - - private var topButtonConfig: NUXButtonConfig? - private var bottomButtonConfig: NUXButtonConfig? - private var tertiaryButtonConfig: NUXButtonConfig? - - public var topButtonStyle: NUXButtonStyle? - public var bottomButtonStyle: NUXButtonStyle? - public var tertiaryButtonStyle: NUXButtonStyle? - - private let style = WordPressAuthenticator.shared.style - - // MARK: - View - - override open func viewDidLoad() { - super.viewDidLoad() - view.translatesAutoresizingMaskIntoConstraints = false - - shadowView?.image = style.buttonViewTopShadowImage - } - - override open func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - configure(button: bottomButton, withConfig: bottomButtonConfig, and: bottomButtonStyle) - configure(button: topButton, withConfig: topButtonConfig, and: topButtonStyle) - configure(button: tertiaryButton, withConfig: tertiaryButtonConfig, and: tertiaryButtonStyle) - - buttonHolder?.backgroundColor = backgroundColor - } - - private func configure(button: NUXButton?, withConfig buttonConfig: NUXButtonConfig?, and style: NUXButtonStyle?) { - if let buttonConfig, let button { - - if let attributedTitle = buttonConfig.attributedTitle { - button.setAttributedTitle(attributedTitle, for: .normal) - button.socialService = buttonConfig.socialService - } else { - button.setTitle(buttonConfig.title, for: .normal) - } - - button.accessibilityIdentifier = buttonConfig.accessibilityIdentifier ?? accessibilityIdentifier(for: buttonConfig.title) - button.isPrimary = buttonConfig.isPrimary - - if buttonConfig.configureBodyFontForTitle == true { - button.customizeFont(WPStyleGuide.mediumWeightFont(forStyle: .body)) - } - - button.buttonStyle = style - - button.isHidden = false - } else { - button?.isHidden = true - } - } - - private func updateShadowViewEdgeConstraints() { - guard let layoutGuide = shadowLayoutGuide, - let shadowView else { - return - } - - NSLayoutConstraint.deactivate(shadowViewEdgeConstraints) - shadowView.translatesAutoresizingMaskIntoConstraints = false - - shadowViewEdgeConstraints = [ - layoutGuide.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor), - layoutGuide.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor), - ] - - NSLayoutConstraint.activate(shadowViewEdgeConstraints) - } - - // MARK: public API - - /// Public method to set the button titles. - /// - /// - Parameters: - /// - primary: Title string for primary button. Required. - /// - primaryAccessibilityId: Accessibility identifier string for primary button. Optional. - /// - secondary: Title string for secondary button. Optional. - /// - secondaryAccessibilityId: Accessibility identifier string for secondary button. Optional. - /// - tertiary: Title string for the tertiary button. Optional. - /// - tertiaryAccessibilityId: Accessibility identifier string for tertiary button. Optional. - /// - public func setButtonTitles(primary: String, primaryAccessibilityId: String? = nil, secondary: String? = nil, secondaryAccessibilityId: String? = nil, tertiary: String? = nil, tertiaryAccessibilityId: String? = nil) { - bottomButtonConfig = NUXButtonConfig(title: primary, isPrimary: true, accessibilityIdentifier: primaryAccessibilityId, callback: nil) - if let secondaryTitle = secondary { - topButtonConfig = NUXButtonConfig(title: secondaryTitle, isPrimary: false, accessibilityIdentifier: secondaryAccessibilityId, callback: nil) - } - if let tertiaryTitle = tertiary { - tertiaryButtonConfig = NUXButtonConfig(title: tertiaryTitle, isPrimary: false, accessibilityIdentifier: tertiaryAccessibilityId, callback: nil) - } - } - - func setupTopButton(title: String, isPrimary: Bool = false, configureBodyFontForTitle: Bool = false, accessibilityIdentifier: String? = nil, onTap callback: @escaping CallBackType) { - topButtonConfig = NUXButtonConfig(title: title, isPrimary: isPrimary, configureBodyFontForTitle: configureBodyFontForTitle, accessibilityIdentifier: accessibilityIdentifier, callback: callback) - } - - func setupTopButtonFor(socialService: SocialServiceName, onTap callback: @escaping CallBackType) { - topButtonConfig = buttonConfigFor(socialService: socialService, onTap: callback) - } - - func setupBottomButton(title: String, isPrimary: Bool = false, configureBodyFontForTitle: Bool = false, accessibilityIdentifier: String? = nil, onTap callback: @escaping CallBackType) { - bottomButtonConfig = NUXButtonConfig(title: title, isPrimary: isPrimary, configureBodyFontForTitle: configureBodyFontForTitle, accessibilityIdentifier: accessibilityIdentifier, callback: callback) - } - - // Sets up bottom button using `NSAttributedString` as title - // - func setupBottomButton(attributedTitle: NSAttributedString, - isPrimary: Bool = false, - configureBodyFontForTitle: Bool = false, - accessibilityIdentifier: String? = nil, - onTap callback: @escaping CallBackType) { - bottomButtonConfig = NUXButtonConfig(attributedTitle: attributedTitle, - isPrimary: isPrimary, - configureBodyFontForTitle: configureBodyFontForTitle, - accessibilityIdentifier: accessibilityIdentifier, - callback: callback) - } - - func setupButtomButtonFor(socialService: SocialServiceName, onTap callback: @escaping CallBackType) { - bottomButtonConfig = buttonConfigFor(socialService: socialService, onTap: callback) - } - - func setupTertiaryButton(attributedTitle: NSAttributedString, isPrimary: Bool = false, accessibilityIdentifier: String? = nil, onTap callback: @escaping CallBackType) { - tertiaryButton?.isHidden = false - tertiaryButtonConfig = NUXButtonConfig(attributedTitle: attributedTitle, isPrimary: isPrimary, accessibilityIdentifier: accessibilityIdentifier, callback: callback) - } - - func setupTertiaryButton(title: String, isPrimary: Bool = false, accessibilityIdentifier: String? = nil, onTap callback: @escaping CallBackType) { - tertiaryButton?.isHidden = false - tertiaryButtonConfig = NUXButtonConfig(title: title, isPrimary: isPrimary, accessibilityIdentifier: accessibilityIdentifier, callback: callback) - } - - func setupTertiaryButtonFor(socialService: SocialServiceName, onTap callback: @escaping CallBackType) { - tertiaryButtonConfig = buttonConfigFor(socialService: socialService, onTap: callback) - } - - func hideShadowView() { - shadowView?.isHidden = true - } - - public func setTopButtonState(isLoading: Bool, isEnabled: Bool) { - topButton?.showActivityIndicator(isLoading) - topButton?.isEnabled = isEnabled - } - - public func setBottomButtonState(isLoading: Bool, isEnabled: Bool) { - bottomButton?.showActivityIndicator(isLoading) - bottomButton?.isEnabled = isEnabled - } - - public func setTertiaryButtonState(isLoading: Bool, isEnabled: Bool) { - tertiaryButton?.showActivityIndicator(isLoading) - tertiaryButton?.isEnabled = isEnabled - } - - // MARK: - Helpers - - private func buttonConfigFor(socialService: SocialServiceName, onTap callback: @escaping CallBackType) -> NUXButtonConfig { - - var attributedTitle = NSAttributedString() - var accessibilityIdentifier = String() - - switch socialService { - case .google: - attributedTitle = WPStyleGuide.formattedGoogleString() - accessibilityIdentifier = "Continue with Google Button" - case .apple: - attributedTitle = WPStyleGuide.formattedAppleString() - accessibilityIdentifier = "Continue with Apple Button" - } - - return NUXButtonConfig(attributedTitle: attributedTitle, - socialService: socialService, - isPrimary: false, - accessibilityIdentifier: accessibilityIdentifier, - callback: callback) - } - - private func accessibilityIdentifier(for string: String?) -> String { - return "\(string ?? "") Button" - } - - // MARK: - Button Handling - - @IBAction func primaryButtonPressed(_ sender: Any) { - guard let callback = bottomButtonConfig?.callback else { - delegate?.primaryButtonPressed() - return - } - callback() - } - - @IBAction func secondaryButtonPressed(_ sender: Any) { - guard let callback = topButtonConfig?.callback else { - delegate?.secondaryButtonPressed() - return - } - callback() - } - - @IBAction func tertiaryButtonPressed(_ sender: Any) { - guard let callback = tertiaryButtonConfig?.callback else { - delegate?.tertiaryButtonPressed() - return - } - callback() - } - - // MARK: - Dynamic type - - func didChangePreferredContentSize() { - configure(button: bottomButton, withConfig: bottomButtonConfig, and: bottomButtonStyle) - configure(button: topButton, withConfig: topButtonConfig, and: topButtonStyle) - configure(button: tertiaryButton, withConfig: tertiaryButtonConfig, and: tertiaryButtonStyle) - } -} - -extension NUXButtonViewController { - override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { - didChangePreferredContentSize() - } - } -} - -extension NUXButtonViewController { - - /// Sets the parentViewControlleras the receiver instance's container. Plus: the containerView will also get the receiver's - /// view, attached to it's edges. This is effectively analog to using an Embed Segue with the NUXButtonViewController. - /// - public func move(to parentViewController: UIViewController, into containerView: UIView) { - containerView.translatesAutoresizingMaskIntoConstraints = false - containerView.addSubview(view) - containerView.pinSubviewToAllEdges(view) - - willMove(toParent: parentViewController) - parentViewController.addChild(self) - didMove(toParent: parentViewController) - } - - /// Returns a new NUXButtonViewController Instance - /// - public class func instance() -> NUXButtonViewController { - let storyboard = UIStoryboard(name: "NUXButtonView", bundle: WordPressAuthenticator.bundle) - guard let buttonViewController = storyboard.instantiateViewController(withIdentifier: "ButtonView") as? NUXButtonViewController else { - fatalError() - } - - return buttonViewController - } -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/Button/NUXStackedButtonsViewController.swift b/Sources/WordPressAuthenticator/Features/NUX/Button/NUXStackedButtonsViewController.swift deleted file mode 100644 index 4cc29dd963a7..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/Button/NUXStackedButtonsViewController.swift +++ /dev/null @@ -1,263 +0,0 @@ -import UIKit -import WordPressShared - -struct StackedButton { - enum StackView { - case top - case bottom - } - - let stackView: StackView - let style: NUXButtonStyle? - - var config: NUXButtonConfig { - NUXButtonConfig(title: title, isPrimary: isPrimary, configureBodyFontForTitle: configureBodyFontForTitle, accessibilityIdentifier: accessibilityIdentifier, callback: onTap) - } - - // MARK: Private properties - private let title: String - private let isPrimary: Bool - private let configureBodyFontForTitle: Bool - private let accessibilityIdentifier: String? - private let onTap: NUXButtonConfig.CallBackType - - init(stackView: StackView = .top, - title: String, - isPrimary: Bool = false, - configureBodyFontForTitle: Bool = false, - accessibilityIdentifier: String? = nil, - style: NUXButtonStyle?, - onTap: @escaping NUXButtonConfig.CallBackType) { - self.stackView = stackView - self.title = title - self.isPrimary = isPrimary - self.configureBodyFontForTitle = configureBodyFontForTitle - self.accessibilityIdentifier = accessibilityIdentifier - self.style = style - self.onTap = onTap - } - - // MARK: Initializers - - /// Initializes a new StackedButton instance using the properties from the provided `StackedButton` and the provided `stackView` - /// - /// Used to copy properties of a StackedButton and just change the stackView placement - /// - /// - Parameters: - /// - using: StackedButton to be copied. (Except the `stackView` property) - /// - stackView: StackView placement of the new StackedButton - init(using: StackedButton, - stackView: StackView) { - self.init(stackView: stackView, - title: using.title, - isPrimary: using.isPrimary, - configureBodyFontForTitle: using.configureBodyFontForTitle, - accessibilityIdentifier: using.accessibilityIdentifier, - style: using.style, - onTap: using.onTap) - } -} - -/// Used to create two stack views of NUXButtons optionally divided by a OR divider -/// -/// Created as a replacement for NUXButtonViewController -/// -open class NUXStackedButtonsViewController: UIViewController { - // MARK: - Properties - @IBOutlet private weak var buttonHolder: UIView? - - // Stack view - @IBOutlet private var topStackView: UIStackView? - @IBOutlet private var bottomStackView: UIStackView? - - // Divider line - @IBOutlet private weak var leadingDividerLine: UIView! - @IBOutlet private weak var leadingDividerLineHeight: NSLayoutConstraint! - @IBOutlet private weak var dividerStackView: UIStackView! - @IBOutlet private weak var dividerLabel: UILabel! - @IBOutlet private weak var trailingDividerLine: UIView! - @IBOutlet private weak var trailingDividerLineHeight: NSLayoutConstraint! - - // Shadow - @IBOutlet private weak var shadowView: UIImageView? - @IBOutlet private var shadowViewEdgeConstraints: [NSLayoutConstraint]! - - /// Used to constrain the shadow view outside of the - /// bounds of this view controller. - weak var shadowLayoutGuide: UILayoutGuide? { - didSet { - updateShadowViewEdgeConstraints() - } - } - - var backgroundColor: UIColor? - private var showDivider = true - private var buttons: [NUXButton] = [] - - private let style = WordPressAuthenticator.shared.style - - private var buttonConfigs = [StackedButton]() - - // MARK: - View - override open func viewDidLoad() { - super.viewDidLoad() - view.translatesAutoresizingMaskIntoConstraints = false - - shadowView?.image = style.buttonViewTopShadowImage - configureDivider() - } - - override open func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - reloadViews() - - buttonHolder?.backgroundColor = backgroundColor - } - - // MARK: public API - func setUpButtons(using config: [StackedButton], showDivider: Bool) { - self.buttonConfigs = config - self.showDivider = showDivider - createButtons() - } - - func hideShadowView() { - shadowView?.isHidden = true - } -} - -// MARK: Helpers -// -private extension NUXStackedButtonsViewController { - @objc func handleTap(_ sender: NUXButton) { - guard let index = buttons.firstIndex(of: sender), - let callback = buttonConfigs[index].config.callback else { - return - } - - callback() - } - - func reloadViews() { - for (index, button) in buttons.enumerated() { - button.configure(withConfig: buttonConfigs[index].config, and: buttonConfigs[index].style) - button.addTarget(self, action: #selector(handleTap), for: .touchUpInside) - } - dividerStackView.isHidden = !showDivider - } - - func createButtons() { - buttons = [] - topStackView?.arrangedSubviews.forEach({ $0.removeFromSuperview() }) - bottomStackView?.arrangedSubviews.forEach({ $0.removeFromSuperview() }) - for config in buttonConfigs { - let button = NUXButton() - switch config.stackView { - case .top: - topStackView?.addArrangedSubview(button) - case .bottom: - bottomStackView?.addArrangedSubview(button) - } - button.configure(withConfig: config.config, and: config.style) - buttons.append(button) - } - } - - func configureDivider() { - guard showDivider else { - return dividerStackView.isHidden = true - } - - leadingDividerLine.backgroundColor = style.orDividerSeparatorColor - leadingDividerLineHeight.constant = WPStyleGuide.hairlineBorderWidth - trailingDividerLine.backgroundColor = style.orDividerSeparatorColor - trailingDividerLineHeight.constant = WPStyleGuide.hairlineBorderWidth - dividerLabel.textColor = style.orDividerTextColor - dividerLabel.text = NSLocalizedString("Or", comment: "Divider on initial auth view separating auth options.").localizedUppercase - } - - func updateShadowViewEdgeConstraints() { - guard let layoutGuide = shadowLayoutGuide, - let shadowView else { - return - } - - NSLayoutConstraint.deactivate(shadowViewEdgeConstraints) - shadowView.translatesAutoresizingMaskIntoConstraints = false - - shadowViewEdgeConstraints = [ - layoutGuide.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor), - layoutGuide.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor), - ] - - NSLayoutConstraint.activate(shadowViewEdgeConstraints) - } - - // MARK: - Dynamic type - func didChangePreferredContentSize() { - reloadViews() - } -} - -extension NUXStackedButtonsViewController { - override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { - didChangePreferredContentSize() - } - } -} - -extension NUXStackedButtonsViewController { - - /// Sets the parentViewControlleras the receiver instance's container. Plus: the containerView will also get the receiver's - /// view, attached to it's edges. This is effectively analog to using an Embed Segue with the NUXButtonViewController. - /// - public func move(to parentViewController: UIViewController, into containerView: UIView) { - containerView.translatesAutoresizingMaskIntoConstraints = false - containerView.addSubview(view) - containerView.pinSubviewToAllEdges(view) - - willMove(toParent: parentViewController) - parentViewController.addChild(self) - didMove(toParent: parentViewController) - } - - /// Returns a new NUXButtonViewController Instance - /// - public class func instance() -> NUXStackedButtonsViewController { - guard let buttonViewController = Storyboard.nuxButtonView.instantiateViewController(ofClass: NUXStackedButtonsViewController.self) else { - fatalError("Cannot instantiate initial NUXStackedButtonsViewController from NUXButtonView.storyboard") - } - - return buttonViewController - } -} - -private extension NUXButton { - func configure(withConfig buttonConfig: NUXButtonConfig?, and style: NUXButtonStyle?) { - guard let buttonConfig else { - isHidden = true - return - } - - if let attributedTitle = buttonConfig.attributedTitle { - setAttributedTitle(attributedTitle, for: .normal) - } else { - setTitle(buttonConfig.title, for: .normal) - } - - socialService = buttonConfig.socialService - accessibilityIdentifier = buttonConfig.accessibilityIdentifier ?? "\(buttonConfig.title ?? "") Button" - isPrimary = buttonConfig.isPrimary - - if buttonConfig.configureBodyFontForTitle == true { - customizeFont(WPStyleGuide.mediumWeightFont(forStyle: .body)) - } - - buttonStyle = style - - isHidden = false - } -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/ModalViewControllerPresenting.swift b/Sources/WordPressAuthenticator/Features/NUX/ModalViewControllerPresenting.swift deleted file mode 100644 index 280511bae000..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/ModalViewControllerPresenting.swift +++ /dev/null @@ -1,7 +0,0 @@ -import UIKit - -protocol ModalViewControllerPresenting { - func present(_ viewControllerToPresent: UIViewController, animated: Bool, completion: (() -> Void)?) -} - -extension UIViewController: ModalViewControllerPresenting {} diff --git a/Sources/WordPressAuthenticator/Features/NUX/NUXKeyboardResponder.swift b/Sources/WordPressAuthenticator/Features/NUX/NUXKeyboardResponder.swift deleted file mode 100644 index 0bf3d386b098..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/NUXKeyboardResponder.swift +++ /dev/null @@ -1,149 +0,0 @@ -import UIKit - -// The signin forms are centered, and then adjusted for the combined height of -// the status bar and navigation bar. -(20 + 44). -// If this value is changed be sure to update the storyboard for consistency. -let NUXKeyboardDefaultFormVerticalOffset: CGFloat = -64.0 - -/// A protocol and extension encapsulating common keyboard releated logic for -/// Signin controllers. -/// -public protocol NUXKeyboardResponder: AnyObject { - var bottomContentConstraint: NSLayoutConstraint? {get} - var verticalCenterConstraint: NSLayoutConstraint? {get} - - func signinFormVerticalOffset() -> CGFloat - func registerForKeyboardEvents(keyboardWillShowAction: Selector, keyboardWillHideAction: Selector) - func unregisterForKeyboardEvents() - func adjustViewForKeyboard(_ visibleKeyboard: Bool) - - func keyboardWillShow(_ notification: Foundation.Notification) - func keyboardWillHide(_ notification: Foundation.Notification) -} - -public extension NUXKeyboardResponder where Self: NUXViewController { - - /// Registeres the receiver for keyboard events using the passed selectors. - /// We pass the selectors this way so we can encapsulate functionality in a - /// Swift protocol extension and still play nice with Objective C code. - /// - /// - Parameters - /// - keyboardWillShowAction: A Selector to use for the UIKeyboardWillShowNotification observer. - /// - keyboardWillHideAction: A Selector to use for the UIKeyboardWillHideNotification observer. - /// - func registerForKeyboardEvents(keyboardWillShowAction: Selector, keyboardWillHideAction: Selector) { - NotificationCenter.default.addObserver(self, selector: keyboardWillShowAction, name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: keyboardWillHideAction, name: UIResponder.keyboardWillHideNotification, object: nil) - } - - /// Unregisters the receiver from keyboard events. - /// - func unregisterForKeyboardEvents() { - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) - } - - /// Returns the vertical offset to apply to the sign in form. - /// - /// - Returns: NUXKeyboardDefaultFormVerticalOffset unless a conforming controller provides its own implementation. - /// - func signinFormVerticalOffset() -> CGFloat { - return NUXKeyboardDefaultFormVerticalOffset - } - - /// Adjusts constraint constants to adapt the view for a visible keyboard. - /// - /// - Parameter visibleKeyboard: Whether to configure for a visible keyboard or without a keyboard. - /// - func adjustViewForKeyboard(_ visibleKeyboard: Bool) { - if visibleKeyboard && SigninEditingState.signinLastKeyboardHeightDelta > 0 { - bottomContentConstraint?.constant = SigninEditingState.signinLastKeyboardHeightDelta - verticalCenterConstraint?.constant = 0 - } else { - bottomContentConstraint?.constant = 0 - verticalCenterConstraint?.constant = signinFormVerticalOffset() - } - } - - /// Process the passed NSNotification from a UIKeyboardWillShowNotification. - /// - /// - Parameter notification: the NSNotification object from a UIKeyboardWillShowNotification. - /// - func keyboardWillShow(_ notification: Foundation.Notification) { - guard let keyboardInfo = keyboardFrameAndDurationFromNotification(notification) else { - return - } - - SigninEditingState.signinLastKeyboardHeightDelta = heightDeltaFromKeyboardFrame(keyboardInfo.keyboardFrame) - SigninEditingState.signinEditingStateActive = true - - if bottomContentConstraint?.constant == SigninEditingState.signinLastKeyboardHeightDelta { - return - } - - adjustViewForKeyboard(true) - UIView.animate(withDuration: keyboardInfo.animationDuration, - delay: 0, - options: .beginFromCurrentState, - animations: { - self.view.layoutIfNeeded() - }, - completion: nil) - } - - /// Process the passed NSNotification from a UIKeyboardWillHideNotification. - /// - /// - Parameter notification: the NSNotification object from a UIKeyboardWillHideNotification. - /// - func keyboardWillHide(_ notification: Foundation.Notification) { - guard let keyboardInfo = keyboardFrameAndDurationFromNotification(notification) else { - return - } - - SigninEditingState.signinEditingStateActive = false - - if bottomContentConstraint?.constant == 0 { - return - } - - adjustViewForKeyboard(false) - UIView.animate(withDuration: keyboardInfo.animationDuration, - delay: 0, - options: .beginFromCurrentState, - animations: { - self.view.layoutIfNeeded() - }, - completion: nil) - } - - /// Retrieves the keyboard frame and the animation duration from a keyboard - /// notificaiton. - /// - /// - Parameter notification: the NSNotification object from a keyboard notification. - /// - /// - Returns: An tupile optional containing the `keyboardFrame` and the `animationDuration`, or nil. - /// - func keyboardFrameAndDurationFromNotification(_ notification: Foundation.Notification) -> (keyboardFrame: CGRect, animationDuration: Double)? { - - guard let userInfo = notification.userInfo, - let frame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue, - let duration = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue - else { - return nil - } - return (keyboardFrame: frame, animationDuration: duration) - } - - func heightDeltaFromKeyboardFrame(_ keyboardFrame: CGRect) -> CGFloat { - // If an external keyboard is connected, the ending keyboard frame's maxY - // will exceed the height of the view controller's view. - // In these cases, just adjust the height by the amount of the keyboard visible. - if keyboardFrame.maxY > UIScreen.main.bounds.size.height { - return view.frame.height - keyboardFrame.minY - } - - // If the safe area has a bottom height, subtract that. - let bottomAdjust: CGFloat = view.safeAreaInsets.bottom - return keyboardFrame.height - bottomAdjust - } -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/NUXLinkAuthViewController.swift b/Sources/WordPressAuthenticator/Features/NUX/NUXLinkAuthViewController.swift deleted file mode 100644 index a212588773dd..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/NUXLinkAuthViewController.swift +++ /dev/null @@ -1,44 +0,0 @@ -import UIKit -import WordPressShared - -/// Handles the final step in the magic link auth process. At this point all the -/// necessary auth work should be done. We just need to create a WPAccount and to -/// sync account info and blog details. -/// The expectation is this controller will be momentarily visible when the app -/// is resumed/launched via the appropriate custom scheme, and quickly dismiss. -/// -class NUXLinkAuthViewController: LoginViewController { - @IBOutlet weak var statusLabel: UILabel? - - enum Flow { - case signup - case login - } - - /// Displays the specified text in the status label. - /// - /// - Parameter message: The text to display in the label. - /// - override func configureStatusLabel(_ message: String) { - statusLabel?.text = message - } - - func syncAndContinue(authToken: String, flow: Flow, isJetpackConnect: Bool) { - let wpcom = WordPressComCredentials(authToken: authToken, isJetpackLogin: isJetpackConnect, multifactor: false, siteURL: "https://wordpress.com") - let credentials = AuthenticatorCredentials(wpcom: wpcom) - - syncWPComAndPresentEpilogue(credentials: credentials) { - self.tracker.track(step: .success) - - switch flow { - case .signup: - // This stat is part of a funnel that provides critical information. Before - // making ANY modification to this stat please refer to: p4qSXL-35X-p2 - WordPressAuthenticator.track(.createdAccount, properties: ["source": "email"]) - WordPressAuthenticator.track(.signupMagicLinkSucceeded) - case .login: - WordPressAuthenticator.track(.loginMagicLinkSucceeded) - } - } - } -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/NUXLinkMailViewController.swift b/Sources/WordPressAuthenticator/Features/NUX/NUXLinkMailViewController.swift deleted file mode 100644 index c91719e1ee5e..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/NUXLinkMailViewController.swift +++ /dev/null @@ -1,128 +0,0 @@ -import UIKit -import WordPressShared - -/// Step two in the auth link flow. This VC prompts the user to open their email -/// app to look for the emailed authentication link. -/// -class NUXLinkMailViewController: LoginViewController { - @IBOutlet private weak var imageView: UIImageView! - @IBOutlet var label: UILabel? - @IBOutlet var openMailButton: NUXButton? - @IBOutlet var usePasswordButton: UIButton? - var emailMagicLinkSource: EmailMagicLinkSource? - override var sourceTag: WordPressSupportSourceTag { - get { - if let emailMagicLinkSource, - emailMagicLinkSource == .signup { - return .wpComSignupMagicLink - } - return .loginMagicLink - } - } - - // MARK: - Lifecycle Methods - - override func viewDidLoad() { - super.viewDidLoad() - - imageView.image = WordPressAuthenticator.shared.displayImages.magicLink - - let email = loginFields.username - if !email.isValidEmail() { - assert(email.isValidEmail(), "The value of loginFields.username was not a valid email address.") - } - - emailMagicLinkSource = loginFields.meta.emailMagicLinkSource - assert(emailMagicLinkSource != nil, "Must have an email link source.") - - styleUsePasswordButton() - localizeControls() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - } - - // MARK: - Configuration - - private func styleUsePasswordButton() { - guard let usePasswordButton else { - return - } - WPStyleGuide.configureTextButton(usePasswordButton) - } - - /// Assigns localized strings to various UIControl defined in the storyboard. - /// - @objc func localizeControls() { - - let openMailButtonTitle = NSLocalizedString("Open Mail", comment: "Title of a button. The text should be capitalized. Clicking opens the mail app in the user's iOS device.") - openMailButton?.setTitle(openMailButtonTitle, for: .normal) - openMailButton?.setTitle(openMailButtonTitle, for: .highlighted) - openMailButton?.accessibilityIdentifier = "Open Mail Button" - - let usePasswordTitle = NSLocalizedString("Enter your password instead.", comment: "Title of a button on the magic link screen.") - usePasswordButton?.setTitle(usePasswordTitle, for: .normal) - usePasswordButton?.setTitle(usePasswordTitle, for: .highlighted) - usePasswordButton?.titleLabel?.numberOfLines = 0 - usePasswordButton?.accessibilityIdentifier = "Use Password" - - guard let emailMagicLinkSource else { - return - } - - usePasswordButton?.isHidden = emailMagicLinkSource == .signup - - label?.text = NSLocalizedString("Check your email on this device, and tap the link in the email you received from WordPress.com.\n\nNot seeing the email? Check your Spam or Junk Mail folder.", comment: "Instructional text on how to open the email containing a magic link.") - - label?.textColor = WordPressAuthenticator.shared.style.instructionColor - } - - // MARK: - Dynamic type - override func didChangePreferredContentSize() { - label?.font = WPStyleGuide.fontForTextStyle(.headline) - } - - // MARK: - Actions - - @IBAction func handleOpenMailTapped(_ sender: UIButton) { - defer { - if let emailMagicLinkSource { - switch emailMagicLinkSource { - case .login: - WordPressAuthenticator.track(.loginMagicLinkOpenEmailClientViewed) - case .signup: - WordPressAuthenticator.track(.signupMagicLinkOpenEmailClientViewed) - } - } - } - - let linkMailPresenter = LinkMailPresenter(emailAddress: loginFields.username) - let appSelector = AppSelector(sourceView: sender) - linkMailPresenter.presentEmailClients(on: self, appSelector: appSelector) - } - - @IBAction func handleUsePasswordTapped(_ sender: UIButton) { - WordPressAuthenticator.track(.loginMagicLinkExited) - guard let vc = LoginWPComViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate to LoginWPComViewController from NUXLinkMailViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } -} - -extension NUXLinkMailViewController { - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { - didChangePreferredContentSize() - } - } -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/NUXNavigationController.swift b/Sources/WordPressAuthenticator/Features/NUX/NUXNavigationController.swift deleted file mode 100644 index 7be8ebcd5bff..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/NUXNavigationController.swift +++ /dev/null @@ -1,7 +0,0 @@ -import UIKit - -/// Simple subclass of UINavigationController to facilitate a customized -/// appearance as part of the sign in flow. -/// -public class NUXNavigationController: RotationAwareNavigationViewController { -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/NUXTableViewController.swift b/Sources/WordPressAuthenticator/Features/NUX/NUXTableViewController.swift deleted file mode 100644 index e7ccf579431e..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/NUXTableViewController.swift +++ /dev/null @@ -1,47 +0,0 @@ -import UIKit - -// MARK: - NUXTableViewController -/// Base class to use for NUX view controllers that are also a table view controller -/// Note: shares most of its code with NUXViewController. -open class NUXTableViewController: UITableViewController, NUXViewControllerBase, UIViewControllerTransitioningDelegate { - // MARK: NUXViewControllerBase properties - /// these properties comply with NUXViewControllerBase and are duplicated with NUXViewController - public var helpButton = UIButton(type: .custom) - public var dismissBlock: ((_ cancelled: Bool) -> Void)? - public var loginFields = LoginFields() - open var sourceTag: WordPressSupportSourceTag { - get { - return .generalLogin - } - } - - override open var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return UIDevice.isPad() ? .all : .portrait - } - - // MARK: - Private - private var notificationObservers: [NSObjectProtocol] = [] - - override open func viewDidLoad() { - super.viewDidLoad() - setupHelpButtonIfNeeded() - setupCancelButtonIfNeeded() - } - - public func shouldShowCancelButton() -> Bool { - return shouldShowCancelButtonBase() - } - - // MARK: - Notification Observers - - public func addNotificationObserver(_ observer: NSObjectProtocol) { - notificationObservers.append(observer) - } - - deinit { - for observer in notificationObservers { - NotificationCenter.default.removeObserver(observer) - } - notificationObservers.removeAll() - } -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/NUXViewController.swift b/Sources/WordPressAuthenticator/Features/NUX/NUXViewController.swift deleted file mode 100644 index 8758f7e04b83..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/NUXViewController.swift +++ /dev/null @@ -1,86 +0,0 @@ -import WordPressUI -import UIKit - -// MARK: - NUXViewController -/// Base class to use for NUX view controllers that aren't a table view -/// Note: shares most of its code with NUXTableViewController. Look to make -/// most changes in either the base protocol NUXViewControllerBase or further subclasses like LoginViewController -open class NUXViewController: UIViewController, NUXViewControllerBase, UIViewControllerTransitioningDelegate { - // MARK: NUXViewControllerBase properties - /// these properties comply with NUXViewControllerBase and are duplicated with NUXTableViewController - public var helpButton = UIButton(type: .custom) - public var dismissBlock: ((_ cancelled: Bool) -> Void)? - public var loginFields = LoginFields() - open var sourceTag: WordPressSupportSourceTag { - get { - return .generalLogin - } - } - - // MARK: - Private - private var notificationObservers: [NSObjectProtocol] = [] - - override open var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return UIDevice.isPad() ? .all : .portrait - } - - override open func viewDidLoad() { - super.viewDidLoad() - setupHelpButtonIfNeeded() - setupCancelButtonIfNeeded() - setupBackgroundTapGestureRecognizer() - } - - // properties specific to NUXViewController - @IBOutlet var submitButton: NUXButton? - @IBOutlet var errorLabel: UILabel? - - func configureSubmitButton(animating: Bool) { - submitButton?.showActivityIndicator(animating) - submitButton?.isEnabled = enableSubmit(animating: animating) - } - - /// Localize the "Continue" button. - /// - func localizePrimaryButton() { - let primaryTitle = WordPressAuthenticator.shared.displayStrings.continueButtonTitle - submitButton?.setTitle(primaryTitle, for: .normal) - submitButton?.setTitle(primaryTitle, for: .highlighted) - submitButton?.accessibilityIdentifier = "Continue Button" - } - - open func enableSubmit(animating: Bool) -> Bool { - return !animating - } - - public func shouldShowCancelButton() -> Bool { - return shouldShowCancelButtonBase() - } - - // MARK: - Notification Observers - - public func addNotificationObserver(_ observer: NSObjectProtocol) { - notificationObservers.append(observer) - } - - deinit { - for observer in notificationObservers { - NotificationCenter.default.removeObserver(observer) - } - notificationObservers.removeAll() - } -} - -extension NUXViewController { - // Required so that any FancyAlertViewControllers presented within the NUX - // use the correct dimmed backing view. - open func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { - if presented is FancyAlertViewController || - presented is LoginPrologueSignupMethodViewController || - presented is LoginPrologueLoginMethodViewController { - return FancyAlertPresentationController(presentedViewController: presented, presenting: presenting) - } - - return nil - } -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/NUXViewControllerBase.swift b/Sources/WordPressAuthenticator/Features/NUX/NUXViewControllerBase.swift deleted file mode 100644 index bf5b54360302..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/NUXViewControllerBase.swift +++ /dev/null @@ -1,223 +0,0 @@ -import Gridicons -import WordPressUI - -private enum Constants { - static let helpButtonInsets = UIEdgeInsets(top: 0.0, left: 5.0, bottom: 0.0, right: 5.0) - // Button Item: Custom view wrapping the Help UIbutton - static let helpButtonItemMarginSpace = CGFloat(-8) - static let helpButtonItemMinimumSize = CGSize(width: 44.0, height: 44.0) - - static let notificationIndicatorCenterOffset = CGPoint(x: 5, y: 12) - static var notificationIndicatorSize = CGSize(width: 10, height: 10) -} - -/// base protocol for NUX view controllers -public protocol NUXViewControllerBase { - var sourceTag: WordPressSupportSourceTag { get } - var helpButton: UIButton { get } - var loginFields: LoginFields { get } - var dismissBlock: ((_ cancelled: Bool) -> Void)? { get } - - /// Checks if the signin vc modal should show a back button. The back button - /// visible when there is more than one child vc presented, and there is not - /// a case where a `SigninChildViewController.backButtonEnabled` in the stack - /// returns false. - /// - /// - Returns: True if the back button should be visible. False otherwise. - /// - func shouldShowCancelButton() -> Bool - func setupCancelButtonIfNeeded() - - /// Notification observers that can be tied to the lifecycle of the entities implementing the protocol - func addNotificationObserver(_ observer: NSObjectProtocol) -} - -/// extension for NUXViewControllerBase where the base class is UIViewController (and thus also NUXTableViewController) -extension NUXViewControllerBase where Self: UIViewController, Self: UIViewControllerTransitioningDelegate { - - /// Indicates if the Help Button should be displayed, or not. - /// - var shouldDisplayHelpButton: Bool { - return WordPressAuthenticator.shared.delegate?.supportActionEnabled ?? false - } - - /// Indicates if the Cancel button should be displayed, or not. - /// - func shouldShowCancelButtonBase() -> Bool { - return isCancellable() && navigationController?.viewControllers.first == self - } - - /// Sets up the cancel button for the navbar if its needed. - /// The cancel button is only shown when its appropriate to dismiss the modal view controller. - /// - public func setupCancelButtonIfNeeded() { - if !shouldShowCancelButton() { - return - } - - let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: nil, action: nil) - cancelButton.on { [weak self] (_: UIBarButtonItem) in - self?.handleCancelButtonTapped() - } - navigationItem.leftBarButtonItem = cancelButton - } - - /// Returns true whenever the current ViewController can be dismissed. - /// - func isCancellable() -> Bool { - return WordPressAuthenticator.shared.delegate?.dismissActionEnabled ?? true - } - - /// Displays a login error in an attractive dialog - /// - func displayError(_ error: Error, sourceTag: WordPressSupportSourceTag) { - let presentingController = navigationController ?? self - let controller = FancyAlertViewController.alertForError(error, loginFields: loginFields, sourceTag: sourceTag) - controller.modalPresentationStyle = .custom - controller.transitioningDelegate = self - presentingController.present(controller, animated: true, completion: nil) - } - - /// Displays a login error message in an attractive dialog - /// - public func displayErrorAlert(_ message: String, sourceTag: WordPressSupportSourceTag, onDismiss: (() -> ())? = nil) { - let presentingController = navigationController ?? self - let controller = FancyAlertViewController.alertForGenericErrorMessageWithHelpButton(message, loginFields: loginFields, sourceTag: sourceTag, onDismiss: onDismiss) - controller.modalPresentationStyle = .custom - controller.transitioningDelegate = self - presentingController.present(controller, animated: true, completion: nil) - } - - /// It is assumed that NUX view controllers are always presented modally. - /// - func dismiss() { - dismiss(cancelled: false) - } - - /// It is assumed that NUX view controllers are always presented modally. - /// This method dismisses the view controller - /// - /// - Parameters: - /// - cancelled: Should be passed true only when dismissed by a tap on the cancel button. - /// - fileprivate func dismiss(cancelled: Bool) { - dismissBlock?(cancelled) - self.dismiss(animated: true, completion: nil) - } - - // MARK: - Actions - - func handleBackgroundTapGesture() { - view.endEditing(true) - } - - func setupBackgroundTapGestureRecognizer() { - let tgr = UITapGestureRecognizer() - tgr.on { [weak self] _ in - self?.handleBackgroundTapGesture() - } - view.addGestureRecognizer(tgr) - } - - func handleCancelButtonTapped() { - dismiss(cancelled: true) - NotificationCenter.default.post(name: .wordpressLoginCancelled, object: nil) - } - - // Handle the help button being tapped - // - func handleHelpButtonTapped(_ sender: AnyObject) { - AuthenticatorAnalyticsTracker.shared.track(click: .showHelp) - - displaySupportViewController(from: sourceTag) - } - - /// Add/remove the nav bar app logo. - /// - func setupNavBarIcon(showIcon: Bool = true) { - showIcon ? addAppLogoToNavController() : removeAppLogoFromNavController() - } - - /// Adds the app logo to the nav controller - /// - public func addAppLogoToNavController() { - let image = WordPressAuthenticator.shared.style.navBarImage - let imageView = UIImageView(image: image.imageWithTintColor(UIColor.white)) - navigationItem.titleView = imageView - } - - /// Removes the app logo from the nav controller - /// - public func removeAppLogoFromNavController() { - navigationItem.titleView = nil - } - - /// Whenever the WordPressAuthenticator Delegate returns true, when `shouldDisplayHelpButton` is queried, we'll proceed - /// and attach the Help Button to the navigationController. - /// - func setupHelpButtonIfNeeded() { - guard shouldDisplayHelpButton else { - return - } - - addHelpButtonToNavController() - } - - // MARK: - Helpers - - /// Adds the Help Button to the nav controller - /// - private func addHelpButtonToNavController() { - let barButtonView = createBarButtonView() - addHelpButton(to: barButtonView) - addRightBarButtonItem(with: barButtonView) - } - - private func addRightBarButtonItem(with customView: UIView) { - let spacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) - spacer.width = Constants.helpButtonItemMarginSpace - - let barButton = UIBarButtonItem(customView: customView) - navigationItem.rightBarButtonItems = [spacer, barButton] - } - - private func createBarButtonView() -> UIView { - let customView = UIView(frame: .zero) - customView.translatesAutoresizingMaskIntoConstraints = false - customView.heightAnchor.constraint(equalToConstant: Constants.helpButtonItemMinimumSize.height).isActive = true - customView.widthAnchor.constraint(greaterThanOrEqualToConstant: Constants.helpButtonItemMinimumSize.width).isActive = true - - return customView - } - - private func addHelpButton(to superView: UIView) { - helpButton.setTitle(NSLocalizedString("Help", comment: "Help button"), for: .normal) - helpButton.setTitleColor(.label, for: []) - helpButton.accessibilityIdentifier = "authenticator-help-button" - - helpButton.on(.touchUpInside) { [weak self] control in - self?.handleHelpButtonTapped(control) - } - - superView.addSubview(helpButton) - helpButton.translatesAutoresizingMaskIntoConstraints = false - - helpButton.leadingAnchor.constraint(equalTo: superView.leadingAnchor, constant: Constants.helpButtonInsets.left).isActive = true - helpButton.trailingAnchor.constraint(equalTo: superView.trailingAnchor, constant: -Constants.helpButtonInsets.right).isActive = true - helpButton.topAnchor.constraint(equalTo: superView.topAnchor).isActive = true - helpButton.bottomAnchor.constraint(equalTo: superView.bottomAnchor).isActive = true - } - - // MARK: - UIViewControllerTransitioningDelegate - - /// Displays the support vc. - /// - func displaySupportViewController(from source: WordPressSupportSourceTag) { - guard let navigationController else { - fatalError() - } - - let state = AuthenticatorAnalyticsTracker.shared.state - WordPressAuthenticator.shared.delegate?.presentSupport(from: navigationController, sourceTag: source, lastStep: state.lastStep, lastFlow: state.lastFlow) - } -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/WPNUXMainButton.h b/Sources/WordPressAuthenticator/Features/NUX/WPNUXMainButton.h deleted file mode 100644 index f747f6035aad..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/WPNUXMainButton.h +++ /dev/null @@ -1,8 +0,0 @@ -#import - -@interface WPNUXMainButton : UIButton - -- (void)showActivityIndicator:(BOOL)show; -- (void)setColor:(UIColor *)color; - -@end diff --git a/Sources/WordPressAuthenticator/Features/NUX/WPNUXMainButton.m b/Sources/WordPressAuthenticator/Features/NUX/WPNUXMainButton.m deleted file mode 100644 index 070f4a93bdf5..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/WPNUXMainButton.m +++ /dev/null @@ -1,81 +0,0 @@ -#import "WPNUXMainButton.h" - -@import WordPressShared; - -@implementation WPNUXMainButton { - UIActivityIndicatorView *activityIndicator; -} - -- (id)initWithFrame:(CGRect)frame -{ - self = [super initWithFrame:frame]; - if (self) { - [self configureButton]; - } - return self; -} - -- (id)initWithCoder:(NSCoder *)aDecoder -{ - self = [super initWithCoder:aDecoder]; - if (self) { - [self configureButton]; - } - return self; -} - -- (void)layoutSubviews -{ - - [super layoutSubviews]; - if ([activityIndicator isAnimating]) { - - // hide the title label when the activity indicator is visible - self.titleLabel.frame = CGRectZero; - activityIndicator.frame = CGRectMake((self.frame.size.width - activityIndicator.frame.size.width) / 2.0, (self.frame.size.height - activityIndicator.frame.size.height) / 2.0, activityIndicator.frame.size.width, activityIndicator.frame.size.height); - } -} - -- (void)configureButton -{ - [self setTitle:NSLocalizedString(@"Log In", nil) forState:UIControlStateNormal]; - [self setTitleColor:[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.9] forState:UIControlStateNormal]; - [self setTitleColor:[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.4] forState:UIControlStateDisabled]; - [self setTitleColor:[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.4] forState:UIControlStateHighlighted]; - self.titleLabel.font = [WPFontManager systemRegularFontOfSize:18.0]; - [self setColor:[UIColor colorWithRed:0/255.0f green:116/255.0f blue:162/255.0f alpha:1.0f]]; - - activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium]; - activityIndicator.hidesWhenStopped = YES; - [self addSubview:activityIndicator]; -} - -- (void)showActivityIndicator:(BOOL)show -{ - if (show) { - [activityIndicator startAnimating]; - } else { - [activityIndicator stopAnimating]; - } - [self setNeedsLayout]; -} - -- (void)setColor:(UIColor *)color -{ - CGRect fillRect = CGRectMake(0, 0, 11.0, 40.0); - UIEdgeInsets capInsets = UIEdgeInsetsMake(4, 4, 4, 4); - UIImage *mainImage; - - UIGraphicsBeginImageContextWithOptions(fillRect.size, NO, [[UIScreen mainScreen] scale]); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextSetFillColorWithColor(context, color.CGColor); - CGContextAddPath(context, [UIBezierPath bezierPathWithRoundedRect:fillRect cornerRadius:3.0].CGPath); - CGContextClip(context); - CGContextFillRect(context, fillRect); - mainImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - [self setBackgroundImage:[mainImage resizableImageWithCapInsets:capInsets] forState:UIControlStateNormal]; -} - -@end diff --git a/Sources/WordPressAuthenticator/Features/NUX/WPWalkthroughTextField.h b/Sources/WordPressAuthenticator/Features/NUX/WPWalkthroughTextField.h deleted file mode 100644 index c70d572c4b56..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/WPWalkthroughTextField.h +++ /dev/null @@ -1,43 +0,0 @@ -#import - -IB_DESIGNABLE -@interface WPWalkthroughTextField : UITextField - -@property (nonatomic) IBInspectable BOOL showTopLineSeparator; -@property (nonatomic) IBInspectable BOOL showSecureTextEntryToggle; -@property (nonatomic) IBInspectable UIImage *leftViewImage; -@property (nonatomic) IBInspectable UIColor *secureTextEntryImageColor; - -/// Width for the left view. Set to 0 to use the given frame in the view. -/// Default is: 30 -/// -@property (nonatomic) CGFloat leadingViewWidth; - -/// Width for the right view. Set to 0 to use the given frame in the view. -/// Default is: 40 -/// -@property (nonatomic) CGFloat trailingViewWidth; - -/// Insets around the text area. -/// This value is mirrored in Right-to-Left layout -/// -@property (nonatomic) UIEdgeInsets textInsets; - -/// Insets around the leading (left) view. -/// This value is mirrored in Right-to-Left layout -/// -@property (nonatomic) UIEdgeInsets leadingViewInsets; - -/// Insets around the trailing (right) view. -/// This value is mirrored in Right-to-Left layout -/// -@property (nonatomic) UIEdgeInsets trailingViewInsets; - -/// Insets around the whole content of the textfield. -/// This value is mirrored in Right-to-Left layout -/// -@property (nonatomic) UIEdgeInsets contentInsets; - -- (instancetype)initWithLeftViewImage:(UIImage *)image; - -@end diff --git a/Sources/WordPressAuthenticator/Features/NUX/WPWalkthroughTextField.m b/Sources/WordPressAuthenticator/Features/NUX/WPWalkthroughTextField.m deleted file mode 100644 index 5a1d2afa039a..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/WPWalkthroughTextField.m +++ /dev/null @@ -1,297 +0,0 @@ -#import "WPWalkthroughTextField.h" - -@import WordPressShared; - -NSInteger const LeftImageSpacing = 8; - -@import Gridicons; - -@interface WPWalkthroughTextField () -@property (nonatomic, strong) UIButton *secureTextEntryToggle; -@property (nonatomic, strong) UIImage *secureTextEntryImageVisible; -@property (nonatomic, strong) UIImage *secureTextEntryImageHidden; -@end - -@implementation WPWalkthroughTextField - -- (instancetype)init -{ - self = [super init]; - if (self) { - [self commonInit]; - } - return self; -} - -- (instancetype)initWithLeftViewImage:(UIImage *)image -{ - self = [self init]; - if (self) { - self.leftViewImage = image; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder -{ - self = [super initWithCoder:aDecoder]; - if (self) { - [self commonInit]; - } - return self; -} - -- (void)setLeftViewImage:(UIImage *)leftViewImage -{ - if (leftViewImage) { - _leftViewImage = leftViewImage; - UIImageView *imageView = [[UIImageView alloc] initWithImage:leftViewImage]; - if (self.leadingViewWidth > 0) { - imageView.frame = [self frameForLeadingView]; - imageView.contentMode = [self isLayoutLeftToRight] ? UIViewContentModeLeft : UIViewContentModeRight; - } else { - [imageView sizeToFit]; - } - self.leftView = imageView; - self.leftViewMode = UITextFieldViewModeAlways; - } else { - self.leftView = nil; - } -} - --(void)setRightView:(UIView *)rightView -{ - if (self.trailingViewWidth > 0) { - rightView.frame = [self frameForTrailingView]; - rightView.contentMode = [self isLayoutLeftToRight] ? UIViewContentModeRight : UIViewContentModeLeft; - if ([rightView isKindOfClass:[UIButton class]]) { - UIButton *button = (UIButton *)rightView; - if ([self isLayoutLeftToRight]) { - [button setContentHorizontalAlignment:UIControlContentHorizontalAlignmentRight]; - } else { - [button setContentHorizontalAlignment:UIControlContentHorizontalAlignmentLeft]; - } - } - } - [super setRightView:rightView]; -} - -- (void)setShowSecureTextEntryToggle:(BOOL)showSecureTextEntryToggle -{ - _showSecureTextEntryToggle = showSecureTextEntryToggle; - [self configureSecureTextEntryToggle]; -} - -- (void)commonInit -{ - self.leadingViewWidth = 30.f; - self.trailingViewWidth = 40.f; - - self.layer.cornerRadius = 0.0; - self.clipsToBounds = YES; - self.showTopLineSeparator = NO; - self.showSecureTextEntryToggle = NO; - - // Apply styles to the placeholder if one was set in IB. - if (self.placeholder) { - // colors here are overridden in LoginTextField - NSDictionary *attributes = @{ - NSForegroundColorAttributeName : WPStyleGuide.greyLighten10, - NSFontAttributeName : self.font, - }; - self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:self.placeholder attributes:attributes]; - } - - self.leadingViewInsets = UIEdgeInsetsMake(0, 0, 0, LeftImageSpacing); -} - -- (void)awakeFromNib { - [super awakeFromNib]; - [self configureSecureTextEntryToggle]; -} - -- (void)configureSecureTextEntryToggle { - if (self.showSecureTextEntryToggle == NO) { - return; - } - self.secureTextEntryImageVisible = [UIImage gridiconOfType:GridiconTypeVisible]; - self.secureTextEntryImageHidden = [UIImage gridiconOfType:GridiconTypeNotVisible]; - - self.secureTextEntryToggle = [UIButton buttonWithType:UIButtonTypeCustom]; - self.secureTextEntryToggle.clipsToBounds = true; - - // Tint color changes set in LoginTextField. - - [self.secureTextEntryToggle addTarget:self action:@selector(secureTextEntryToggleAction:) forControlEvents:UIControlEventTouchUpInside]; - - [self updateSecureTextEntryToggleImage]; - [self updateSecureTextEntryForAccessibility]; - - self.rightView = self.secureTextEntryToggle; - self.rightViewMode = UITextFieldViewModeAlways; -} - -- (CGSize)intrinsicContentSize -{ - return CGSizeMake(0.0, 44.0); -} - -- (void)drawRect:(CGRect)rect -{ - // Draw top border - if (!self.showTopLineSeparator) { - return; - } - - CGContextRef context = UIGraphicsGetCurrentContext(); - - UIBezierPath *path = [UIBezierPath bezierPath]; - CGFloat emptySpace = self.contentInsets.left; - if ([self isLayoutLeftToRight]) { - [path moveToPoint:CGPointMake(CGRectGetMinX(rect) + emptySpace, CGRectGetMinY(rect))]; - [path addLineToPoint:CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect))]; - } else { - [path moveToPoint:CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect))]; - [path addLineToPoint:CGPointMake(CGRectGetMaxX(rect) - emptySpace, CGRectGetMinY(rect))]; - } - - [path setLineWidth:[[UIScreen mainScreen] scale] / 2.0]; - CGContextAddPath(context, path.CGPath); - CGContextSetStrokeColorWithColor(context, [UIColor colorWithWhite:0.87 alpha:1.0].CGColor); - CGContextStrokePath(context); -} - - -/// Returns the drawing rectangle for the text field’s text. -/// -- (CGRect)textRectForBounds:(CGRect)bounds -{ - CGRect rect = [super textRectForBounds:bounds]; - return [self textAreaRectForProposedRect:rect]; -} - -/// Returns the rectangle in which editable text can be displayed. -/// -- (CGRect)editingRectForBounds:(CGRect)bounds -{ - CGRect rect = [super editingRectForBounds:bounds]; - return [self textAreaRectForProposedRect:rect]; -} - -/// Returns the drawing rectangle of the receiver’s left overlay view. -/// This value is always the view seen at the left side, independently of the layout direction. -/// -- (CGRect)leftViewRectForBounds:(CGRect)bounds -{ - CGRect rect = [super leftViewRectForBounds:bounds]; - if ([self isLayoutLeftToRight]) { - rect.origin.x += self.leadingViewInsets.left + self.contentInsets.left; - } else { - rect.origin.x += self.trailingViewInsets.right + self.contentInsets.right; - } - return rect; -} - -/// Returns the drawing location of the receiver’s right overlay view. -/// This value is always the view seen at the right side, independently of the layout direction. -/// -- (CGRect)rightViewRectForBounds:(CGRect)bounds -{ - CGRect rect = [super rightViewRectForBounds:bounds]; - if ([self isLayoutLeftToRight]) { - rect.origin.x -= self.trailingViewInsets.right + self.contentInsets.right; - } else { - rect.origin.x -= self.leadingViewInsets.left + self.contentInsets.left; - } - return rect; -} - -#pragma mark - Helpers - -- (BOOL)isLayoutLeftToRight -{ - return [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:self.semanticContentAttribute] == UIUserInterfaceLayoutDirectionLeftToRight; -} - -/// Returns the rectangle in which both editable text and the placeholder can be displayed. -/// -- (CGRect)textAreaRectForProposedRect:(CGRect)rect -{ - rect.size.width -= self.textInsets.left + self.textInsets.right; - if ([self isLayoutLeftToRight]) { - rect.origin.x += self.textInsets.left + self.leadingViewInsets.right; - rect.size.width -= self.leadingViewInsets.right + self.contentInsets.right; - if (self.leftView == nil) { - rect.origin.x += self.contentInsets.left; - rect.size.width -= self.contentInsets.right; - } - } else { - rect.origin.x += self.textInsets.right + self.trailingViewInsets.left; - rect.size.width -= self.leadingViewInsets.right + self.trailingViewInsets.left; - if (self.rightView == nil) { - rect.origin.x += self.contentInsets.right; - rect.size.width -= self.contentInsets.left; - } - if (self.leftView == nil) { - rect.size.width -= self.contentInsets.left; - } - } - return rect; -} - -- (CGRect)frameForTrailingView -{ - return CGRectMake(0, 0, self.trailingViewWidth, CGRectGetHeight(self.bounds)); -} - -- (CGRect)frameForLeadingView -{ - return CGRectMake(0, 0, self.leadingViewWidth, CGRectGetHeight(self.bounds)); -} - -#pragma mark - Secure Text Entry - -- (void)setSecureTextEntry:(BOOL)secureTextEntry -{ - // This is a fix for a bug where the text field reverts to a system - // serif font if you disable secure text entry while it contains text. - self.font = nil; - self.font = [WPFontManager systemRegularFontOfSize:16.0]; - - [super setSecureTextEntry:secureTextEntry]; - [self updateSecureTextEntryToggleImage]; - [self updateSecureTextEntryForAccessibility]; -} - -- (void)secureTextEntryToggleAction:(id)sender -{ - self.secureTextEntry = !self.secureTextEntry; - - // Save and re-apply the current selection range to save the cursor position - UITextRange *currentTextRange = self.selectedTextRange; - [self becomeFirstResponder]; - [self setSelectedTextRange:currentTextRange]; -} - -- (void)updateSecureTextEntryToggleImage -{ - UIImage *image = self.isSecureTextEntry ? self.secureTextEntryImageHidden : self.secureTextEntryImageVisible; - [self.secureTextEntryToggle setImage:image forState:UIControlStateNormal]; - [self.secureTextEntryToggle sizeToFit]; - self.secureTextEntryToggle.tintColor = self.secureTextEntryImageColor; -} - -- (void)updateSecureTextEntryForAccessibility -{ - self.secureTextEntryToggle.accessibilityLabel = NSLocalizedString(@"Show password", @"Accessibility label for the “Show password“ button in the login page's password field."); - - NSString *accessibilityValue; - if (self.isSecureTextEntry) { - accessibilityValue = NSLocalizedString(@"Hidden", "Accessibility value if login page's password field is hiding the password (i.e. with asterisks)."); - } else { - accessibilityValue = NSLocalizedString(@"Shown", "Accessibility value if login page's password field is displaying the password."); - } - self.secureTextEntryToggle.accessibilityValue = accessibilityValue; -} - -@end diff --git a/Sources/WordPressAuthenticator/Features/SignIn/AppleAuthenticator.swift b/Sources/WordPressAuthenticator/Features/SignIn/AppleAuthenticator.swift deleted file mode 100644 index bf5af489264e..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/AppleAuthenticator.swift +++ /dev/null @@ -1,291 +0,0 @@ -import Foundation -import AuthenticationServices -import WordPressKit -import SVProgressHUD -import WordPressShared - -@objc protocol AppleAuthenticatorDelegate { - func showWPComLogin(loginFields: LoginFields) - func showApple2FA(loginFields: LoginFields) - func authFailedWithError(message: String) -} - -class AppleAuthenticator: NSObject { - - // MARK: - Properties - - static var sharedInstance = AppleAuthenticator() - private var showFromViewController: UIViewController? - private let loginFields = LoginFields() - weak var delegate: AppleAuthenticatorDelegate? - let signupService: SocialUserCreating - - init(signupService: SocialUserCreating = SignupService()) { - self.signupService = signupService - super.init() - } - - static let credentialRevokedNotification = ASAuthorizationAppleIDProvider.credentialRevokedNotification - - private var tracker: AuthenticatorAnalyticsTracker { - AuthenticatorAnalyticsTracker.shared - } - - private var authenticationDelegate: WordPressAuthenticatorDelegate { - guard let delegate = WordPressAuthenticator.shared.delegate else { - fatalError() - } - return delegate - } - - // MARK: - Start Authentication - - func showFrom(viewController: UIViewController) { - loginFields.meta.socialService = SocialServiceName.apple - showFromViewController = viewController - requestAuthorization() - } -} - -// MARK: - Tracking - -private extension AppleAuthenticator { - func track(_ event: WPAnalyticsStat, properties: [AnyHashable: Any] = [:]) { - var trackProperties = properties - trackProperties["source"] = "apple" - WordPressAuthenticator.track(event, properties: trackProperties) - } -} - -// MARK: - Authentication Flow - -private extension AppleAuthenticator { - - func requestAuthorization() { - let provider = ASAuthorizationAppleIDProvider() - let request = provider.createRequest() - request.requestedScopes = [.fullName, .email] - - let controller = ASAuthorizationController(authorizationRequests: [request]) - controller.delegate = self - - controller.presentationContextProvider = self - controller.performRequests() - } - - /// Creates a WordPress.com account with the Apple ID - /// - func createWordPressComUser(appleCredentials: ASAuthorizationAppleIDCredential) { - guard let identityToken = appleCredentials.identityToken, - let token = String(data: identityToken, encoding: .utf8) else { - WPLogError("Apple Authenticator: invalid Apple credentials.") - return - } - - createWordPressComUser( - appleUserId: appleCredentials.user, - email: appleCredentials.email ?? "", - name: fullName(from: appleCredentials.fullName), - token: token - ) - } - - func signupSuccessful(with credentials: AuthenticatorCredentials) { - // This stat is part of a funnel that provides critical information. Before - // making ANY modification to this stat please refer to: p4qSXL-35X-p2 - track(.createdAccount) - - tracker.track(step: .success) { - track(.signupSocialSuccess) - } - - showSignupEpilogue(for: credentials) - } - - func loginSuccessful(with credentials: AuthenticatorCredentials) { - // This stat is part of a funnel that provides critical information. Please - // consult with your lead before removing this event. - track(.signedIn) - - tracker.track(step: .success) { - track(.loginSocialSuccess) - } - - showLoginEpilogue(for: credentials) - } - - func showLoginEpilogue(for credentials: AuthenticatorCredentials) { - guard let navigationController = showFromViewController?.navigationController else { - fatalError() - } - - authenticationDelegate.presentLoginEpilogue(in: navigationController, - for: credentials, - source: WordPressAuthenticator.shared.signInSource) {} - } - - func signupFailed(with error: Error) { - WPLogError("Apple Authenticator: Signup failed. error: \(error.localizedDescription)") - - let errorMessage = error.localizedDescription - - tracker.track(failure: errorMessage) { - let properties = ["error": errorMessage] - track(.signupSocialFailure, properties: properties) - } - - delegate?.authFailedWithError(message: error.localizedDescription) - } - - func logInInstead() { - tracker.set(flow: .loginWithApple) - tracker.track(step: .start) { - track(.signupSocialToLogin) - track(.loginSocialSuccess) - } - - delegate?.showWPComLogin(loginFields: loginFields) - } - - func show2FA() { - if tracker.shouldUseLegacyTracker() { - track(.signupSocialToLogin) - } - - delegate?.showApple2FA(loginFields: loginFields) - } - - // MARK: - Helpers - - func fullName(from components: PersonNameComponents?) -> String { - guard let name = components else { - return "" - } - return PersonNameComponentsFormatter().string(from: name) - } - - func updateLoginFields(email: String, fullName: String, token: String) { - updateLoginEmail(email) - loginFields.meta.socialServiceIDToken = token - loginFields.meta.socialUser = SocialUser(email: email, fullName: fullName, service: .apple) - } - - func updateLoginEmail(_ email: String) { - loginFields.emailAddress = email - loginFields.username = email - } -} - -extension AppleAuthenticator: ASAuthorizationControllerDelegate { - - func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { - switch authorization.credential { - case let credentials as ASAuthorizationAppleIDCredential: - createWordPressComUser(appleCredentials: credentials) - default: - break - } - } - - func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { - - // Don't show error if user cancelled authentication. - if let authorizationError = error as? ASAuthorizationError, - authorizationError.code == .canceled { - return - } - - WPLogError("Apple Authenticator: didCompleteWithError: \(error.localizedDescription)") - let message = NSLocalizedString("Apple authentication failed.\nPlease make sure you are signed in to iCloud with an Apple ID that uses two-factor authentication.", comment: "Message shown when Apple authentication fails.") - delegate?.authFailedWithError(message: message) - } -} - -extension AppleAuthenticator: ASAuthorizationControllerPresentationContextProviding { - func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - return showFromViewController?.view.window ?? UIWindow() - } -} - -extension AppleAuthenticator { - func getAppleIDCredentialState(for userID: String, - completion: @escaping (ASAuthorizationAppleIDProvider.CredentialState, Error?) -> Void) { - ASAuthorizationAppleIDProvider().getCredentialState(forUserID: userID, completion: completion) - } -} - -// This needs to be internal, at this point in time, to allow testing. -// -// Notice that none of this code was previously tested. A small encapsulation breach like this is -// worth the testability we gain from it. -extension AppleAuthenticator { - - func showSignupEpilogue(for credentials: AuthenticatorCredentials) { - guard let navigationController = showFromViewController?.navigationController else { - fatalError() - } - - authenticationDelegate.presentSignupEpilogue( - in: navigationController, - for: credentials, - socialUser: loginFields.meta.socialUser - ) - } - - func createWordPressComUser(appleUserId: String, email: String, name: String, token: String) { - tracker.set(flow: .signupWithApple) - tracker.track(step: .start) { - track(.createAccountInitiated) - } - - SVProgressHUD.show( - withStatus: NSLocalizedString( - "Continuing with Apple", - comment: "Shown while logging in with Apple and the app waits for the site creation process to complete." - ) - ) - - updateLoginFields(email: email, fullName: name, token: token) - - signupService.createWPComUserWithApple( - token: token, - email: email, - fullName: name, - success: { [weak self] accountCreated, existingNonSocialAccount, existing2faAccount, wpcomUsername, wpcomToken in - SVProgressHUD.dismiss() - - // Notify host app of successful Apple authentication - self?.authenticationDelegate.userAuthenticatedWithAppleUserID(appleUserId) - - guard !existingNonSocialAccount else { - self?.tracker.set(flow: .loginWithApple) - - if existing2faAccount { - self?.show2FA() - return - } - - self?.updateLoginEmail(wpcomUsername) - self?.logInInstead() - return - } - - let wpcom = WordPressComCredentials(authToken: wpcomToken, isJetpackLogin: false, multifactor: false, siteURL: self?.loginFields.siteAddress ?? "") - let credentials = AuthenticatorCredentials(wpcom: wpcom) - - if accountCreated { - self?.authenticationDelegate.createdWordPressComAccount(username: wpcomUsername, authToken: wpcomToken) - self?.signupSuccessful(with: credentials) - } else { - self?.authenticationDelegate.sync(credentials: credentials) { - self?.loginSuccessful(with: credentials) - } - } - }, - failure: { [weak self] error in - SVProgressHUD.dismiss() - self?.signupFailed(with: error) - } - ) - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/EmailMagicLink.storyboard b/Sources/WordPressAuthenticator/Features/SignIn/EmailMagicLink.storyboard deleted file mode 100644 index 505e6e7f0b0d..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/EmailMagicLink.storyboard +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Features/SignIn/Login.storyboard b/Sources/WordPressAuthenticator/Features/SignIn/Login.storyboard deleted file mode 100644 index 0b039b2f923b..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/Login.storyboard +++ /dev/null @@ -1,1481 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Features/SignIn/Login2FAViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/Login2FAViewController.swift deleted file mode 100644 index f8cb96047ae6..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/Login2FAViewController.swift +++ /dev/null @@ -1,326 +0,0 @@ -import UIKit -import SVProgressHUD -import WordPressShared -import WordPressKit - -/// Provides a form and functionality for entering a two factor auth code and -/// signing into WordPress.com -/// -class Login2FAViewController: LoginViewController, NUXKeyboardResponder, UITextFieldDelegate { - - @IBOutlet weak var verificationCodeField: LoginTextField! - @IBOutlet weak var sendCodeButton: UIButton! - @IBOutlet var bottomContentConstraint: NSLayoutConstraint? - @IBOutlet var verticalCenterConstraint: NSLayoutConstraint? - - private var pasteboardChangeCountBeforeBackground: Int = 0 - override var sourceTag: WordPressSupportSourceTag { - get { - return .login2FA - } - } - - private enum Constants { - static let headsUpDismissDelay = TimeInterval(1) - } - - // MARK: - Lifecycle Methods - - override func viewDidLoad() { - super.viewDidLoad() - - localizeControls() - configureTextFields() - configureSubmitButton(animating: false) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - configureViewForEditingIfNeeded() - styleSendCodeButton() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - - let nc = NotificationCenter.default - nc.addObserver(self, selector: #selector(applicationBecameInactive), name: UIApplication.willResignActiveNotification, object: nil) - nc.addObserver(self, selector: #selector(applicationBecameActive), name: UIApplication.didBecomeActiveNotification, object: nil) - - WordPressAuthenticator.track(.loginTwoFactorFormViewed) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - NotificationCenter.default.removeObserver(self) - - // Multifactor codes are time sensitive, so clear the stored code if the - // user dismisses the view. They'll need to reentered it upon return. - loginFields.multifactorCode = "" - verificationCodeField.text = "" - } - - // MARK: Dynamic Type - override func didChangePreferredContentSize() { - super.didChangePreferredContentSize() - styleSendCodeButton() - } - - private func styleSendCodeButton() { - sendCodeButton.titleLabel?.adjustsFontForContentSizeCategory = true - sendCodeButton.titleLabel?.adjustsFontSizeToFitWidth = true - WPStyleGuide.configureTextButton(sendCodeButton) - } - - // MARK: Configuration Methods - - /// Assigns localized strings to various UIControl defined in the storyboard. - /// - @objc func localizeControls() { - instructionLabel?.text = NSLocalizedString("Almost there! Please enter the verification code from your authenticator app.", comment: "Instructions for users with two-factor authentication enabled.") - - verificationCodeField.placeholder = NSLocalizedString("Verification code", comment: "two factor code placeholder") - - let submitButtonTitle = NSLocalizedString("Next", comment: "Title of a button.").localizedCapitalized - submitButton?.setTitle(submitButtonTitle, for: .normal) - submitButton?.setTitle(submitButtonTitle, for: .highlighted) - - sendCodeButton.setTitle(NSLocalizedString("Text me a code instead", comment: "Button title"), - for: .normal) - sendCodeButton.titleLabel?.numberOfLines = 0 - } - - /// configures the text fields - /// - @objc func configureTextFields() { - verificationCodeField.contentInsets = WPStyleGuide.edgeInsetForLoginTextFields() - verificationCodeField.textContentType = .oneTimeCode - } - - /// Configures the appearance and state of the submit button. - /// - override func configureSubmitButton(animating: Bool) { - submitButton?.showActivityIndicator(animating) - - let isNumeric = loginFields.multifactorCode.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil - let isValidLength = SocialLogin2FANonceInfo.TwoFactorTypeLengths(rawValue: loginFields.multifactorCode.count) != nil - - submitButton?.isEnabled = ( - !animating && - isNumeric && - isValidLength - ) - } - - /// Configure the view's loading state. - /// - /// - Parameter loading: True if the form should be configured to a "loading" state. - /// - override func configureViewLoading(_ loading: Bool) { - verificationCodeField.enablesReturnKeyAutomatically = !loading - - configureSubmitButton(animating: loading) - navigationItem.hidesBackButton = loading - } - - /// Configure the view for an editing state. Should only be called from viewWillAppear - /// as this method skips animating any change in height. - /// - @objc func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editiing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - verificationCodeField.becomeFirstResponder() - } - } - - // MARK: - Instance Methods - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with the submit action. - /// - @objc func validateForm() { - if let nonce = loginFields.nonceInfo { - loginWithNonce(info: nonce) - return - } - validateFormAndLogin() - } - - private func loginWithNonce(info nonceInfo: SocialLogin2FANonceInfo) { - let code = loginFields.multifactorCode - let (authType, nonce) = nonceInfo.authTypeAndNonce(for: code) - loginFacade.loginToWordPressDotCom(withUser: loginFields.nonceUserID, authType: authType, twoStepCode: code, twoStepNonce: nonce) - } - - func finishedLogin(withNonceAuthToken authToken: String) { - let wpcom = WordPressComCredentials(authToken: authToken, isJetpackLogin: isJetpackLogin, multifactor: true, siteURL: loginFields.siteAddress) - let credentials = AuthenticatorCredentials(wpcom: wpcom) - syncWPComAndPresentEpilogue(credentials: credentials) - - // This stat is part of a funnel that provides critical information. Please - // consult with your lead before removing this event. - WordPressAuthenticator.track(.signedIn) - - var properties = [AnyHashable: Any]() - if let service = loginFields.meta.socialService?.rawValue { - properties["source"] = service - } - - WordPressAuthenticator.track(.loginSocialSuccess, properties: properties) - } - - /// Only allow digits in the 2FA text field - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString: String) -> Bool { - guard let fieldText = textField.text as NSString? else { - return true - } - let resultString = fieldText.replacingCharacters(in: range, with: replacementString) - - switch isValidCode(code: resultString) { - case .valid(let cleanedCode): - displayError(message: "") - - // because the string was stripped of whitespace, we can't return true and we update the textfield ourselves - textField.text = cleanedCode - handleTextFieldDidChange(textField) - case .invalid(nonNumbers: true): - displayError(message: NSLocalizedString("A verification code will only contain numbers.", comment: "Shown when a user types a non-number into the two factor field.")) - default: - if let pasteString = UIPasteboard.general.string, pasteString == replacementString { - displayError(message: NSLocalizedString("That doesn't appear to be a valid verification code.", comment: "Shown when a user pastes a code into the two factor field that contains letters or is the wrong length")) - } - } - - return false - } - - private enum CodeValidation { - case invalid(nonNumbers: Bool) - case valid(String) - } - - private func isValidCode(code: String) -> CodeValidation { - let codeStripped = code.components(separatedBy: .whitespacesAndNewlines).joined() - let allowedCharacters = CharacterSet.decimalDigits - let resultCharacterSet = CharacterSet(charactersIn: codeStripped) - let isOnlyNumbers = allowedCharacters.isSuperset(of: resultCharacterSet) - let isShortEnough = codeStripped.count <= SocialLogin2FANonceInfo.TwoFactorTypeLengths.backup.rawValue - - if isOnlyNumbers && isShortEnough { - return .valid(codeStripped) - } else if isOnlyNumbers { - return .invalid(nonNumbers: false) - } else { - return .invalid(nonNumbers: true) - } - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - validateForm() - return false - } - - @IBAction func handleTextFieldDidChange(_ sender: UITextField) { - loginFields.multifactorCode = verificationCodeField.nonNilTrimmedText() - configureSubmitButton(animating: false) - } - - // MARK: - Actions - - @IBAction func handleSubmitForm() { - validateForm() - } - - @IBAction func handleSubmitButtonTapped(_ sender: UIButton) { - tracker.track(click: .submit) - - validateForm() - } - - @IBAction func handleSendVerificationButtonTapped(_ sender: UIButton) { - self.tracker.track(click: .sendCodeWithText) - - let message = NSLocalizedString("SMS Sent", comment: "One Time Code has been sent via SMS") - SVProgressHUD.showSuccess(withStatus: message) - SVProgressHUD.dismiss(withDelay: Constants.headsUpDismissDelay) - - if let _ = loginFields.nonceInfo { - // social login - loginFacade.requestSocial2FACode(with: loginFields) - } else { - loginFacade.requestOneTimeCode(with: loginFields) - } - } - - // MARK: - Handle application state changes. - - @objc func applicationBecameInactive() { - pasteboardChangeCountBeforeBackground = UIPasteboard.general.changeCount - } - - @objc func applicationBecameActive() { - let emptyField = verificationCodeField.text?.isEmpty ?? true - guard emptyField, - pasteboardChangeCountBeforeBackground != UIPasteboard.general.changeCount else { - return - } - - UIPasteboard.general.detectAuthenticatorCode { [weak self] result in - switch result { - case .success(let authenticatorCode): - self?.handle(code: authenticatorCode) - case .failure: - break - } - } - } - - private func handle(code: String) { - switch isValidCode(code: code) { - case .valid(let cleanedCode): - displayError(message: "") - verificationCodeField.text = cleanedCode - handleTextFieldDidChange(verificationCodeField) - default: - break - } - } - - // MARK: - Keyboard Notifications - - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } -} - -extension Login2FAViewController { - - override func displayRemoteError(_ error: Error) { - displayError(message: "") - - configureViewLoading(false) - let bad2FAMessage = NSLocalizedString("Whoops, that's not a valid two-factor verification code. Double-check your code and try again!", comment: "Error message shown when an incorrect two factor code is provided.") - if (error as? WordPressComOAuthError)?.authenticationFailureKind == .invalidOneTimePassword { - // Invalid verification code. - displayError(message: bad2FAMessage) - } else if case let .endpointError(authenticationFailure) = (error as? WordPressComOAuthError), authenticationFailure.kind == .invalidTwoStepCode { - // Invalid 2FA during social login - if let newNonce = authenticationFailure.newNonce { - loginFields.nonceInfo?.updateNonce(with: newNonce) - } - displayError(message: bad2FAMessage) - } else { - displayError(error, sourceTag: sourceTag) - } - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginEmailViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginEmailViewController.swift deleted file mode 100644 index d9dd2782191e..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginEmailViewController.swift +++ /dev/null @@ -1,631 +0,0 @@ -import UIKit -import WordPressShared -import WordPressKit - -/// This is the first screen following the log in prologue screen if the user chooses to log in. -/// -open class LoginEmailViewController: LoginViewController, NUXKeyboardResponder { - @IBOutlet var emailTextField: WPWalkthroughTextField! - @IBOutlet open var bottomContentConstraint: NSLayoutConstraint? - @IBOutlet open var verticalCenterConstraint: NSLayoutConstraint? - @IBOutlet var inputStack: UIStackView? - @IBOutlet var alternativeLoginLabel: UILabel? - @IBOutlet var hiddenPasswordField: WPWalkthroughTextField? - - var googleLoginButton: UIButton? - var selfHostedLoginButton: UIButton? - - // This signup button isn't for the main flow; it's only shown during Jetpack installation - var wpcomSignupButton: UIButton? - - override open var sourceTag: WordPressSupportSourceTag { - get { - return .loginEmail - } - } - - var didFindSafariSharedCredentials = false - var didRequestSafariSharedCredentials = false - open var offerSignupOption = false - private let showLoginOptions = WordPressAuthenticator.shared.configuration.showLoginOptions - - private struct Constants { - static let alternativeLogInAnimationDuration: TimeInterval = 0.33 - static let keyboardThreshold: CGFloat = 100.0 - } - - // MARK: Lifecycle Methods - - override open func viewDidLoad() { - super.viewDidLoad() - - localizeControls() - - alternativeLoginLabel?.isHidden = showLoginOptions - if !showLoginOptions { - addGoogleButton() - } - - addSelfHostedLogInButton() - addSignupButton() - } - - override open func didChangePreferredContentSize() { - super.didChangePreferredContentSize() - configureEmailField() - configureAlternativeLabel() - } - - override open func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - // The old create account vc hides the nav bar, so make sure its always visible. - navigationController?.setNavigationBarHidden(false, animated: false) - - // Update special case login fields. - loginFields.meta.userIsDotCom = true - - configureEmailField() - configureAlternativeLabel() - configureSubmitButton() - configureViewForEditingIfNeeded() - configureForWPComOnlyIfNeeded() - } - - override open func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - - WordPressAuthenticator.track(.loginEmailFormViewed) - - hiddenPasswordField?.text = nil - errorToPresent = nil - } - - override open func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - } - - /// Displays the self-hosted login form. - /// - override func loginToSelfHostedSite() { - guard let vc = LoginSiteAddressViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginEmailViewController to LoginSiteAddressViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - // MARK: - Setup and Configuration - - /// Hides the self-hosted login option. - /// - func configureForWPComOnlyIfNeeded() { - wpcomSignupButton?.isHidden = !offerSignupOption - selfHostedLoginButton?.isHidden = loginFields.restrictToWPCom - } - - /// Assigns localized strings to various UIControl defined in the storyboard. - /// - func localizeControls() { - if loginFields.meta.jetpackLogin { - instructionLabel?.text = WordPressAuthenticator.shared.displayStrings.jetpackLoginInstructions - } else { - instructionLabel?.text = WordPressAuthenticator.shared.displayStrings.emailLoginInstructions - } - emailTextField.placeholder = NSLocalizedString("Email address", comment: "Placeholder for a textfield. The user may enter their email address.") - emailTextField.accessibilityIdentifier = "Login Email Address" - - alternativeLoginLabel?.text = NSLocalizedString("Alternatively:", comment: "String displayed before offering alternative login methods") - - let submitButtonTitle = NSLocalizedString("Next", comment: "Title of a button. The text should be capitalized.").localizedCapitalized - submitButton?.setTitle(submitButtonTitle, for: .normal) - submitButton?.setTitle(submitButtonTitle, for: .highlighted) - submitButton?.accessibilityIdentifier = "Login Email Next Button" - } - - /// Add the log in with Google button to the view - /// - func addGoogleButton() { - guard let instructionLabel, - let stackView = inputStack else { - return - } - - let button = WPStyleGuide.googleLoginButton() - stackView.addArrangedSubview(button) - button.addTarget(self, action: #selector(googleTapped), for: .touchUpInside) - - stackView.addConstraints([ - button.leadingAnchor.constraint(equalTo: instructionLabel.leadingAnchor), - button.trailingAnchor.constraint(equalTo: instructionLabel.trailingAnchor) - ]) - - googleLoginButton = button - } - - /// Add the log in with site address button to the view - /// - func addSelfHostedLogInButton() { - guard let instructionLabel, - let stackView = inputStack else { - return - } - - let button = WPStyleGuide.selfHostedLoginButton() - stackView.addArrangedSubview(button) - button.addTarget(self, action: #selector(handleSelfHostedButtonTapped), for: .touchUpInside) - - stackView.addConstraints([ - button.leadingAnchor.constraint(equalTo: instructionLabel.leadingAnchor), - button.trailingAnchor.constraint(equalTo: instructionLabel.trailingAnchor) - ]) - - selfHostedLoginButton = button - } - - /// Add the sign up button - /// - /// Note: This is only used during Jetpack setup, not the normal flows - /// - func addSignupButton() { - guard let instructionLabel, - let stackView = inputStack else { - return - } - - let button = WPStyleGuide.wpcomSignupButton() - stackView.addArrangedSubview(button) - - // Tapping the Sign up text link in "Don't have an account? _Sign up_" - // will present the 3 button view for signing up. - button.on(.touchUpInside) { [weak self] _ in - guard let vc = LoginPrologueSignupMethodViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate to LoginPrologueSignupMethodViewController") - return - } - - guard let self else { return } - - vc.loginFields = self.loginFields - vc.dismissBlock = self.dismissBlock - vc.transitioningDelegate = self - vc.modalPresentationStyle = .custom - - // Don't forget to handle the button taps! - vc.emailTapped = { [weak self] in - guard let toVC = SignupEmailViewController.instantiate(from: .signup) else { - WPLogError("Failed to navigate from LoginEmailViewController to SignupEmailViewController") - return - } - - self?.navigationController?.pushViewController(toVC, animated: true) - } - - vc.googleTapped = { [weak self] in - guard let self else { - return - } - - self.tracker.track(click: .signupWithGoogle) - - guard WordPressAuthenticator.shared.configuration.enableUnifiedAuth else { - self.presentGoogleSignupView() - return - } - - self.presentUnifiedGoogleView() - } - - vc.appleTapped = { [weak self] in - self?.appleTapped() - } - - self.navigationController?.present(vc, animated: true, completion: nil) - } - - stackView.addConstraints([ - button.leadingAnchor.constraint(equalTo: instructionLabel.leadingAnchor), - button.trailingAnchor.constraint(equalTo: instructionLabel.trailingAnchor) - ]) - - wpcomSignupButton = button - } - - /// Configures the email text field, updating its text based on what's stored - /// in `loginFields`. - /// - func configureEmailField() { - emailTextField.contentInsets = WPStyleGuide.edgeInsetForLoginTextFields() - emailTextField.text = loginFields.username - emailTextField.adjustsFontForContentSizeCategory = true - hiddenPasswordField?.isAccessibilityElement = false - } - - private func configureAlternativeLabel() { - alternativeLoginLabel?.font = WPStyleGuide.fontForTextStyle(.subheadline) - alternativeLoginLabel?.textColor = WordPressAuthenticator.shared.style.subheadlineColor - } - - /// Configures whether appearance of the submit button. - /// - func configureSubmitButton() { - submitButton?.isEnabled = canSubmit() - } - - /// Sets the view's state to loading or not loading. - /// - /// - Parameter loading: True if the form should be configured to a "loading" state. - /// - override open func configureViewLoading(_ loading: Bool) { - emailTextField.isEnabled = !loading - googleLoginButton?.isEnabled = !loading - - submitButton?.isEnabled = !loading - submitButton?.showActivityIndicator(loading) - } - - /// Configure the view for an editing state. Should only be called from viewWillAppear - /// as this method skips animating any change in height. - /// - func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editiing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - emailTextField.becomeFirstResponder() - } - } - - // MARK: - Instance Methods - - /// Makes the call to retrieve Safari shared credentials if they exist. - /// - func fetchSharedWebCredentialsIfAvailable() { - didRequestSafariSharedCredentials = true - SafariCredentialsService.requestSharedWebCredentials { [weak self] found, username, password in - self?.handleFetchedWebCredentials(found, username: username, password: password) - } - } - - /// Handles Safari shared credentials if any where found. - /// - /// - Parameters: - /// - found: True if credentails were found. - /// - username: The selected username or nil. - /// - password: The selected password or nil. - /// - func handleFetchedWebCredentials(_ found: Bool, username: String?, password: String?) { - didFindSafariSharedCredentials = found - - guard let username, let password else { - return - } - - // Update the login fields - loginFields.username = username - loginFields.password = password - - // Persist credentials as autofilled credentials so we can update them later if needed. - loginFields.setStoredCredentials(usernameHash: username.hash, passwordHash: password.hash) - - loginWithUsernamePassword(immediately: true) - - WordPressAuthenticator.track(.loginAutoFillCredentialsFilled) - } - - /// Displays the wpcom sign in form, optionally telling it to immedately make - /// the call to authenticate with the available credentials. - /// - /// - Parameters: - /// - immediately: True if the newly loaded controller should immedately attempt - /// to authenticate the user with the available credentails. Default is `false`. - /// - func loginWithUsernamePassword(immediately: Bool = false) { - if immediately { - validateFormAndLogin() - } else { - guard let vc = LoginWPComViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginEmailViewController to LoginWPComViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - } - - /// Proceeds along the "magic link" sign-in flow, showing a form that lets - /// the user request a magic link. - /// - func requestLink() { - guard let vc = LoginLinkRequestViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginEmailViewController to LoginLinkRequestViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with the submit action. Empties loginFields.meta.socialService as - /// social signin does not require form validation. - /// - func validateForm() { - loginFields.meta.socialService = nil - displayError(message: "") - - guard EmailFormatValidator.validate(string: loginFields.username) else { - assertionFailure("Form should not be submitted unless there is a valid looking email entered.") - return - } - - configureViewLoading(true) - let service = WordPressComAccountService() - service.isPasswordlessAccount(username: loginFields.username, - success: { [weak self] passwordless in - self?.configureViewLoading(false) - self?.loginFields.meta.passwordless = passwordless - self?.requestLink() - }, - failure: { [weak self] error in - WordPressAuthenticator.track(.loginFailed, error: error) - WPLogError(error.localizedDescription) - guard let strongSelf = self else { - return - } - strongSelf.configureViewLoading(false) - - let userInfo = (error as NSError).userInfo - let errorCode = userInfo[WordPressComRestApi.ErrorKeyErrorCode] as? String - if errorCode == "unknown_user" { - let msg = NSLocalizedString("This email address is not registered on WordPress.com.", - comment: "An error message informing the user the email address they entered did not match a WordPress.com account.") - strongSelf.displayError(message: msg) - } else if errorCode == "email_login_not_allowed" { - // If we get this error, we know we have a WordPress.com user but their - // email address is flagged as suspicious. They need to login via their - // username instead. - strongSelf.showSelfHostedUsernamePasswordAndError(error) - } else { - strongSelf.displayError(error, sourceTag: strongSelf.sourceTag) - } - }) - } - - /// When password autofill has entered a password on this screen, attempt to login immediately - func attemptAutofillLogin() { - loginFields.password = hiddenPasswordField?.text ?? "" - loginFields.meta.socialService = nil - displayError(message: "") - - loginWithUsernamePassword(immediately: true) - } - - /// Configures loginFields to log into wordpress.com and - /// navigates to the selfhosted username/password form. - /// Displays the specified error message when the new - /// view controller appears. - /// - @objc func showSelfHostedUsernamePasswordAndError(_ error: Error) { - loginFields.siteAddress = "https://wordpress.com" - errorToPresent = error - - guard let vc = LoginSelfHostedViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginEmailViewController to LoginSelfHostedViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - /// Whether the form can be submitted. - /// - func canSubmit() -> Bool { - return EmailFormatValidator.validate(string: loginFields.username) - } - - // MARK: - Actions - - @IBAction func handleSubmitForm() { - if canSubmit() { - validateForm() - } - } - - @IBAction func handleSubmitButtonTapped(_ sender: UIButton) { - validateForm() - } - - @IBAction func handleSelfHostedButtonTapped(_ sender: UIButton) { - loginToSelfHostedSite() - } - - private func appleTapped() { - AppleAuthenticator.sharedInstance.delegate = self - AppleAuthenticator.sharedInstance.showFrom(viewController: self) - } - - @objc func googleTapped() { - self.tracker.track(click: .loginWithGoogle) - - guard WordPressAuthenticator.shared.configuration.enableUnifiedAuth else { - GoogleAuthenticator.sharedInstance.loginDelegate = self - GoogleAuthenticator.sharedInstance.showFrom(viewController: self, loginFields: loginFields, for: .login) - return - } - - presentUnifiedGoogleView() - } - - // Shows the VC that handles both Google login & signup. - private func presentUnifiedGoogleView() { - guard let toVC = GoogleAuthViewController.instantiate(from: .googleAuth) else { - WPLogError("Failed to navigate to GoogleAuthViewController from LoginPrologueVC") - return - } - - navigationController?.pushViewController(toVC, animated: true) - } - - // Shows the VC that handles only Google signup. - private func presentGoogleSignupView() { - guard let toVC = SignupGoogleViewController.instantiate(from: .signup) else { - WPLogError("Failed to navigate to SignupGoogleViewController from LoginEmailVC") - return - } - - navigationController?.pushViewController(toVC, animated: true) - } - - @IBAction func handleTextFieldDidChange(_ sender: UITextField) { - switch sender { - case emailTextField: - loginFields.username = emailTextField.nonNilTrimmedText() - configureSubmitButton() - case hiddenPasswordField: - attemptAutofillLogin() - default: - break - } - } - - @IBAction func handleTextFieldEditingDidBegin(_ sender: UITextField) { - if !didRequestSafariSharedCredentials { - fetchSharedWebCredentialsIfAvailable() - } - } - - // MARK: - Keyboard Notifications - - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - - adjustAlternativeLogInElementsVisibility(true) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - - adjustAlternativeLogInElementsVisibility(false) - } - - func adjustAlternativeLogInElementsVisibility(_ visible: Bool) { - let errorLength = errorLabel?.text?.count ?? 0 - let keyboardTallEnough = SigninEditingState.signinLastKeyboardHeightDelta > Constants.keyboardThreshold - let keyboardVisible = visible && keyboardTallEnough - - let baseAlpha: CGFloat = errorLength > 0 ? 0.0 : 1.0 - let newAlpha: CGFloat = keyboardVisible ? baseAlpha : 1.0 - - UIView.animate(withDuration: Constants.alternativeLogInAnimationDuration) { [weak self] in - self?.alternativeLoginLabel?.alpha = newAlpha - self?.googleLoginButton?.alpha = newAlpha - if let selfHostedLoginButton = self?.selfHostedLoginButton, - selfHostedLoginButton.isEnabled { - selfHostedLoginButton.alpha = newAlpha - } - } - } -} - -// MARK: - AppleAuthenticatorDelegate - -extension LoginEmailViewController: AppleAuthenticatorDelegate { - - func showWPComLogin(loginFields: LoginFields) { - self.loginFields = loginFields - - guard let vc = LoginWPComViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginEmailViewController to LoginWPComViewController") - return - } - - vc.loginFields = self.loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - func showApple2FA(loginFields: LoginFields) { - self.loginFields = loginFields - signInAppleAccount() - } - - func authFailedWithError(message: String) { - displayErrorAlert(message, sourceTag: .loginApple) - } -} - -// MARK: - GoogleAuthenticatorLoginDelegate - -extension LoginEmailViewController: GoogleAuthenticatorLoginDelegate { - - func googleFinishedLogin(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - self.loginFields = loginFields - syncWPComAndPresentEpilogue(credentials: credentials) - } - - func googleNeedsMultifactorCode(loginFields: LoginFields) { - self.loginFields = loginFields - configureViewLoading(false) - - guard let vc = Login2FAViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginViewController to Login2FAViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - func googleExistingUserNeedsConnection(loginFields: LoginFields) { - self.loginFields = loginFields - configureViewLoading(false) - - guard let vc = LoginWPComViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from Google Login to LoginWPComViewController (password VC)") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - func googleLoginFailed(errorTitle: String, errorDescription: String, loginFields: LoginFields) { - self.loginFields = loginFields - configureViewLoading(false) - - let socialErrorVC = LoginSocialErrorViewController(title: errorTitle, description: errorDescription) - let socialErrorNav = LoginNavigationController(rootViewController: socialErrorVC) - socialErrorVC.delegate = self - socialErrorVC.loginFields = loginFields - socialErrorVC.modalPresentationStyle = .fullScreen - present(socialErrorNav, animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginLinkRequestViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginLinkRequestViewController.swift deleted file mode 100644 index aa1c8042a0d4..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginLinkRequestViewController.swift +++ /dev/null @@ -1,188 +0,0 @@ -import UIKit -import WordPressShared -import WordPressUI -import GravatarUI - -/// Step one in the auth link flow. This VC displays a form to request a "magic" -/// authentication link be emailed to the user. Allows the user to signin via -/// email instead of their password. -/// -class LoginLinkRequestViewController: LoginViewController { - @IBOutlet var gravatarView: UIImageView? - @IBOutlet var label: UILabel? - @IBOutlet var sendLinkButton: NUXButton? - @IBOutlet var usePasswordButton: UIButton? - override var sourceTag: WordPressSupportSourceTag { - get { - return .loginMagicLink - } - } - - // MARK: - Lifecycle Methods - - override func viewDidLoad() { - super.viewDidLoad() - - localizeControls() - configureUsePasswordButton() - - let email = loginFields.username - if !email.isValidEmail() { - assert(email.isValidEmail(), "The value of loginFields.username was not a valid email address.") - } - NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarQEAvatarUpdateNotification, object: nil) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - let email = loginFields.username - if email.isValidEmail() { - Task { - try await downloadAvatar() - } - } else { - gravatarView?.isHidden = true - } - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - WordPressAuthenticator.track(.loginMagicLinkRequestFormViewed) - } - - private func downloadAvatar(forceRefresh: Bool = false) async throws { - let email = loginFields.username - try await gravatarView?.setGravatarImage(with: email, rating: .x, forceRefresh: forceRefresh) - } - - @objc private func refreshAvatar(_ notification: Foundation.Notification) { - guard notification.userInfoHasEmail(loginFields.username) else { return } - Task { - try await downloadAvatar(forceRefresh: true) - } - } - - // MARK: - Configuration - - /// Assigns localized strings to various UIControl defined in the storyboard. - /// - @objc func localizeControls() { - let format = NSLocalizedString("We'll email you a magic link that'll log you in instantly, no password needed. Hunt and peck no more!", comment: "Instructional text for the magic link login flow.") - label?.text = NSString(format: format as NSString, loginFields.username) as String - label?.font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - label?.textColor = WordPressAuthenticator.shared.style.instructionColor - label?.adjustsFontForContentSizeCategory = true - - let sendLinkButtonTitle = NSLocalizedString("Send Link", comment: "Title of a button. The text should be uppercase. Clicking requests a hyperlink be emailed ot the user.") - sendLinkButton?.setTitle(sendLinkButtonTitle, for: .normal) - sendLinkButton?.setTitle(sendLinkButtonTitle, for: .highlighted) - sendLinkButton?.accessibilityIdentifier = "Send Link Button" - - let usePasswordTitle = NSLocalizedString("Enter your password instead.", comment: "Title of a button. ") - usePasswordButton?.setTitle(usePasswordTitle, for: .normal) - usePasswordButton?.setTitle(usePasswordTitle, for: .highlighted) - usePasswordButton?.titleLabel?.numberOfLines = 0 - usePasswordButton?.titleLabel?.textAlignment = .center - usePasswordButton?.accessibilityIdentifier = "Use Password" - } - - @objc func configureLoading(_ animating: Bool) { - sendLinkButton?.showActivityIndicator(animating) - - sendLinkButton?.isEnabled = !animating - } - - private func configureUsePasswordButton() { - guard let usePasswordButton else { - return - } - WPStyleGuide.configureTextButton(usePasswordButton) - } - - // MARK: - Instance Methods - - /// Makes the call to request a magic authentication link be emailed to the user. - /// - @objc func requestAuthenticationLink() { - - loginFields.meta.emailMagicLinkSource = .login - - let email = loginFields.username - guard email.isValidEmail() else { - // This is a bit of paranoia as in practice it should never happen. - // However, let's make sure we give the user some useful feedback just in case. - WPLogError("Attempted to request authentication link, but the email address did not appear valid.") - let alert = UIAlertController(title: NSLocalizedString("Can Not Request Link", comment: "Title of an alert letting the user know"), message: NSLocalizedString("A valid email address is needed to mail an authentication link. Please return to the previous screen and provide a valid email address.", comment: "An error message."), preferredStyle: .alert) - alert.addActionWithTitle(NSLocalizedString("Need help?", comment: "Takes the user to get help"), style: .cancel, handler: { _ in WordPressAuthenticator.shared.delegate?.presentSupportRequest(from: self, sourceTag: .loginEmail) }) - alert.addActionWithTitle(NSLocalizedString("OK", comment: "Dismisses the alert"), style: .default, handler: nil) - self.present(alert, animated: true, completion: nil) - return - } - - configureLoading(true) - let service = WordPressComAccountService() - service.requestAuthenticationLink(for: email, - jetpackLogin: loginFields.meta.jetpackLogin, - success: { [weak self] in - self?.didRequestAuthenticationLink() - self?.configureLoading(false) - }, failure: { [weak self] (error: Error) in - WordPressAuthenticator.track(.loginMagicLinkFailed) - WordPressAuthenticator.track(.loginFailed, error: error) - guard let strongSelf = self else { - return - } - strongSelf.displayError(error, sourceTag: strongSelf.sourceTag) - strongSelf.configureLoading(false) - }) - } - - // MARK: - Dynamic type - override func didChangePreferredContentSize() { - label?.font = WPStyleGuide.fontForTextStyle(.headline) - } - - // MARK: - Actions - - @IBAction func handleUsePasswordTapped(_ sender: UIButton) { - guard let vc = LoginWPComViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginLinkRequestViewController to LoginWPComViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - WordPressAuthenticator.track(.loginMagicLinkExited) - } - - @IBAction func handleSendLinkTapped(_ sender: UIButton) { - requestAuthenticationLink() - } - - @objc func didRequestAuthenticationLink() { - WordPressAuthenticator.track(.loginMagicLinkRequested) - - guard let vc = NUXLinkMailViewController.instantiate(from: .emailMagicLink) else { - WPLogError("Failed to navigate to NUXLinkMailViewController") - return - } - - vc.loginFields = self.loginFields - vc.loginFields.restrictToWPCom = true - navigationController?.pushViewController(vc, animated: true) - } -} - -extension LoginLinkRequestViewController { - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { - didChangePreferredContentSize() - } - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginNavigationController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginNavigationController.swift deleted file mode 100644 index ae36a3ecd556..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginNavigationController.swift +++ /dev/null @@ -1,39 +0,0 @@ -import UIKit -import WordPressShared - -public class LoginNavigationController: RotationAwareNavigationViewController { - - public override var preferredStatusBarStyle: UIStatusBarStyle { - return topViewController?.preferredStatusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } - - public override func pushViewController(_ viewController: UIViewController, animated: Bool) { - // By default, the back button label uses the previous view's title. - // To override that, reset the label when pushing a new view controller. - self.viewControllers.last?.navigationItem.backButtonDisplayMode = .minimal - - super.pushViewController(viewController, animated: animated) - } -} - -// MARK: - RotationAwareNavigationViewController -// -public class RotationAwareNavigationViewController: UINavigationController { - - /// Should Autorotate: Respect the top child's orientation prefs. - /// - override open var shouldAutorotate: Bool { - return topViewController?.shouldAutorotate ?? super.shouldAutorotate - } - - /// Supported Orientations: Respect the top child's orientation prefs. - /// - override open var supportedInterfaceOrientations: UIInterfaceOrientationMask { - if let supportedOrientations = topViewController?.supportedInterfaceOrientations { - return supportedOrientations - } - - let isPad = UIDevice.current.userInterfaceIdiom == .pad - return isPad ? .all : .allButUpsideDown - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueLoginMethodViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueLoginMethodViewController.swift deleted file mode 100644 index 9be4d611c1fc..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueLoginMethodViewController.swift +++ /dev/null @@ -1,132 +0,0 @@ -import WordPressUI -import WordPressShared - -/// This class houses the "3 button view": -/// Continue with WordPress.com, Continue with Google, Continue with Apple -/// and a text link - Or log in by entering your site address. -/// -class LoginPrologueLoginMethodViewController: NUXViewController { - /// Buttons at bottom of screen - private var buttonViewController: NUXButtonViewController? - - /// Gesture recognizer for taps on the dialog if no buttons are present - fileprivate var dismissGestureRecognizer: UITapGestureRecognizer? - - open var emailTapped: (() -> Void)? - open var googleTapped: (() -> Void)? - open var selfHostedTapped: (() -> Void)? - open var appleTapped: (() -> Void)? - - private var tracker: AuthenticatorAnalyticsTracker { - AuthenticatorAnalyticsTracker.shared - } - - /// The big transparent (dismiss) button behind the buttons - @IBOutlet private weak var dismissButton: UIButton! - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - super.prepare(for: segue, sender: sender) - - if let vc = segue.destination as? NUXButtonViewController { - buttonViewController = vc - } - } - - override func viewDidLoad() { - super.viewDidLoad() - configureButtonVC() - configureForAccessibility() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - self.navigationController?.setNavigationBarHidden(true, animated: false) - } - - private func configureButtonVC() { - guard let buttonViewController else { - return - } - - let wordpressTitle = NSLocalizedString("Log in or sign up with WordPress.com", comment: "Button title. Tapping begins our normal log in process.") - buttonViewController.setupTopButton(title: wordpressTitle, isPrimary: false, accessibilityIdentifier: "Log in with Email Button") { [weak self] in - - guard let self else { - return - } - - self.tracker.set(flow: .wpCom) - self.dismiss(animated: true) - self.emailTapped?() - } - - buttonViewController.setupButtomButtonFor(socialService: .google) { [weak self] in - self?.handleGoogleButtonTapped() - } - - if !LoginFields().restrictToWPCom && selfHostedTapped != nil { - let selfHostedLoginButton = WPStyleGuide.selfHostedLoginButton(alignment: .center) - buttonViewController.stackView?.addArrangedSubview(selfHostedLoginButton) - selfHostedLoginButton.addTarget(self, action: #selector(handleSelfHostedButtonTapped), for: .touchUpInside) - } - - if WordPressAuthenticator.shared.configuration.enableSignInWithApple { - buttonViewController.setupTertiaryButtonFor(socialService: .apple) { [weak self] in - self?.handleAppleButtonTapped() - } - } - - buttonViewController.backgroundColor = WordPressAuthenticator.shared.style.buttonViewBackgroundColor - } - - @IBAction func dismissTapped() { - dismiss(animated: true) - } - - @IBAction func handleSelfHostedButtonTapped(_ sender: UIButton) { - dismiss(animated: true) - - tracker.set(flow: .loginWithSiteAddress) - tracker.track(click: .loginWithSiteAddress) - - selfHostedTapped?() - } - - @objc func handleAppleButtonTapped() { - tracker.set(flow: .loginWithApple) - tracker.track(click: .loginWithApple, ifTrackingNotEnabled: { - WordPressAuthenticator.track(.loginSocialButtonClick, properties: ["source": "apple"]) - }) - - dismiss(animated: true) - appleTapped?() - } - - @objc func handleGoogleButtonTapped() { - tracker.set(flow: .loginWithGoogle) - tracker.track(click: .loginWithGoogle) - - dismiss(animated: true) - googleTapped?() - } - - // MARK: - Accessibility - - private func configureForAccessibility() { - dismissButton.accessibilityLabel = NSLocalizedString("Dismiss", comment: "Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog.") - - // Ensure that the first button (in buttonViewController) is automatically selected by - // VoiceOver instead of the dismiss button. - if buttonViewController?.isViewLoaded == true, let buttonsView = buttonViewController?.view { - view.accessibilityElements = [ - buttonsView, - dismissButton as Any - ] - } - } - - override func accessibilityPerformEscape() -> Bool { - dismiss(animated: true) - return true - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginProloguePageViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginProloguePageViewController.swift deleted file mode 100644 index 1e9875e32d82..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginProloguePageViewController.swift +++ /dev/null @@ -1,98 +0,0 @@ -import UIKit -import WordPressShared - -class LoginProloguePageViewController: UIPageViewController { - // This property is a legacy of the previous UX iteration. It ought to be removed, but that's - // out of scope at the time of writing. It's now `private` to prevent using it within the - // library in the meantime - @objc private var pages: [UIViewController] = [] - - fileprivate var pageControl: UIPageControl? - fileprivate var bgAnimation: UIViewPropertyAnimator? - fileprivate struct Constants { - static let pagerPadding: CGFloat = 9.0 - static let pagerHeight: CGFloat = 0.13 - } - - override func viewDidLoad() { - super.viewDidLoad() - dataSource = self - delegate = self - - view.backgroundColor = WordPressAuthenticator.shared.style.prologueBackgroundColor - - addPageControl() - } - - @objc func addPageControl() { - let newControl = UIPageControl() - - newControl.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(newControl) - - newControl.topAnchor.constraint(equalTo: view.topAnchor, constant: Constants.pagerPadding).isActive = true - newControl.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true - newControl.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true - newControl.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: Constants.pagerHeight).isActive = true - - newControl.numberOfPages = pages.count - newControl.addTarget(self, action: #selector(handlePageControlValueChanged(sender:)), for: .valueChanged) - pageControl = newControl - } - - @objc func handlePageControlValueChanged(sender: UIPageControl) { - guard let currentPage = viewControllers?.first, - let currentIndex = pages.firstIndex(of: currentPage) else { - return - } - - let direction: UIPageViewController.NavigationDirection = sender.currentPage > currentIndex ? .forward : .reverse - setViewControllers([pages[sender.currentPage]], direction: direction, animated: true) - WordPressAuthenticator.track(.loginProloguePaged) - } -} - -extension LoginProloguePageViewController: UIPageViewControllerDataSource { - - func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { - guard let index = pages.firstIndex(of: viewController) else { - return nil - } - if index > 0 { - return pages[index - 1] - } - return nil - } - - func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { - guard let index = pages.firstIndex(of: viewController) else { - return nil - } - if index < pages.count - 1 { - return pages[index + 1] - } - return nil - } -} - -extension LoginProloguePageViewController: UIPageViewControllerDelegate { - func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { - let toVC = previousViewControllers[0] - guard let index = pages.firstIndex(of: toVC) else { - return - } - if !completed { - pageControl?.currentPage = index - } else { - WordPressAuthenticator.track(.loginProloguePaged) - } - } - - func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { - let toVC = pendingViewControllers[0] - guard let index = pages.firstIndex(of: toVC) else { - return - } - pageControl?.currentPage = index - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueSignupMethodViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueSignupMethodViewController.swift deleted file mode 100644 index 65279d774bb1..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueSignupMethodViewController.swift +++ /dev/null @@ -1,134 +0,0 @@ -import SafariServices -import WordPressUI -import WordPressShared - -class LoginPrologueSignupMethodViewController: NUXViewController { - /// Buttons at bottom of screen - private var buttonViewController: NUXButtonViewController? - - /// Gesture recognizer for taps on the dialog if no buttons are present - fileprivate var dismissGestureRecognizer: UITapGestureRecognizer? - - open var emailTapped: (() -> Void)? - open var googleTapped: (() -> Void)? - open var appleTapped: (() -> Void)? - - private var tracker: AuthenticatorAnalyticsTracker { - AuthenticatorAnalyticsTracker.shared - } - - /// The big transparent (dismiss) button behind the buttons - @IBOutlet private weak var dismissButton: UIButton! - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - super.prepare(for: segue, sender: sender) - - if let vc = segue.destination as? NUXButtonViewController { - buttonViewController = vc - } - } - - override func viewDidLoad() { - super.viewDidLoad() - configureButtonVC() - configureForAccessibility() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - self.navigationController?.setNavigationBarHidden(true, animated: false) - } - - private func configureButtonVC() { - guard let buttonViewController else { - return - } - - let loginTitle = NSLocalizedString("Sign up with Email", comment: "Button title. Tapping begins our normal sign up process.") - buttonViewController.setupTopButton(title: loginTitle, isPrimary: false, accessibilityIdentifier: "Sign up with Email Button") { [weak self] in - - self?.tracker.set(flow: .wpCom) - - defer { - WordPressAuthenticator.track(.signupEmailButtonTapped) - } - self?.dismiss(animated: true) - self?.emailTapped?() - } - - buttonViewController.setupButtomButtonFor(socialService: .google) { [weak self] in - self?.handleGoogleButtonTapped() - } - - let termsButton = WPStyleGuide.termsButton() - termsButton.on(.touchUpInside) { [weak self] _ in - defer { - self?.tracker.track(click: .termsOfService, ifTrackingNotEnabled: { - WordPressAuthenticator.track(.signupTermsButtonTapped) - }) - } - - let safariViewController = SFSafariViewController(url: WordPressAuthenticator.shared.configuration.wpcomTermsOfServiceURL) - safariViewController.modalPresentationStyle = .pageSheet - self?.present(safariViewController, animated: true, completion: nil) - } - buttonViewController.stackView?.insertArrangedSubview(termsButton, at: 0) - - if WordPressAuthenticator.shared.configuration.enableSignInWithApple { - buttonViewController.setupTertiaryButtonFor(socialService: .apple) { [weak self] in - self?.handleAppleButtonTapped() - } - } - - buttonViewController.backgroundColor = WordPressAuthenticator.shared.style.buttonViewBackgroundColor - } - - @IBAction func dismissTapped() { - trackCancellationAndThenDismiss() - } - - @objc func handleAppleButtonTapped() { - tracker.set(flow: .signupWithApple) - tracker.track(click: .signupWithApple, ifTrackingNotEnabled: { - WordPressAuthenticator.track(.signupSocialButtonTapped, properties: ["source": "apple"]) - }) - - dismiss(animated: true) - appleTapped?() - } - - @objc func handleGoogleButtonTapped() { - tracker.set(flow: .signupWithGoogle) - tracker.track(click: .signupWithGoogle, ifTrackingNotEnabled: { - WordPressAuthenticator.track(.signupSocialButtonTapped, properties: ["source": "google"]) - }) - - dismiss(animated: true) - googleTapped?() - } - - private func trackCancellationAndThenDismiss() { - WordPressAuthenticator.track(.signupCancelled) - dismiss(animated: true) - } - - // MARK: - Accessibility - - private func configureForAccessibility() { - dismissButton.accessibilityLabel = NSLocalizedString("Dismiss", comment: "Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog.") - - // Ensure that the first button (in buttonViewController) is automatically selected by - // VoiceOver instead of the dismiss button. - if buttonViewController?.isViewLoaded == true, let buttonsView = buttonViewController?.view { - view.accessibilityElements = [ - buttonsView, - dismissButton as Any - ] - } - } - - override func accessibilityPerformEscape() -> Bool { - trackCancellationAndThenDismiss() - return true - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueViewController.swift deleted file mode 100644 index e25b3a8fe1f8..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueViewController.swift +++ /dev/null @@ -1,739 +0,0 @@ -import UIKit -import WordPressShared -import WordPressUI -import WordPressKit - -class LoginPrologueViewController: LoginViewController { - - @IBOutlet private weak var topContainerView: UIView! - @IBOutlet private weak var buttonBlurEffectView: UIVisualEffectView! - @IBOutlet private weak var buttonBackgroundView: UIView! - private var buttonViewController: NUXButtonViewController? - private var stackedButtonsViewController: NUXStackedButtonsViewController? - var showCancel = false - var continueWithDotComOverwrite: ((UIViewController) -> Bool)? = nil - var selfHostedSiteLoginOverwrite: ((UIViewController) -> Bool)? = nil - - @IBOutlet private weak var buttonContainerView: UIView! - /// Blur effect on button container view - /// - private var blurEffect: UIBlurEffect.Style { - return .systemChromeMaterial - } - - /// Constraints on the button view container. - /// Used to adjust the button width in unified views. - @IBOutlet private weak var buttonViewLeadingConstraint: NSLayoutConstraint? - @IBOutlet private weak var buttonViewTrailingConstraint: NSLayoutConstraint? - private var defaultButtonViewMargin: CGFloat = 0 - - // Called when login button is tapped - var onLoginButtonTapped: (() -> Void)? - - private let configuration = WordPressAuthenticator.shared.configuration - private let style = WordPressAuthenticator.shared.style - - /// We can't rely on `isMovingToParent` to know if we need to track the `.prologue` step - /// because for the root view in an App, it's always `false`. We're relying this variable - /// instead, since the `.prologue` step only needs to be tracked once. - /// - private var prologueFlowTracked = false - - /// Return`true` to use new `NUXStackedButtonsViewController` instead of `NUXButtonViewController` to create buttons - /// - private var useStackedButtonsViewController: Bool { - configuration.enableWPComLoginOnlyInPrologue || - configuration.enableSiteCreation || - configuration.enableSiteAddressLoginOnlyInPrologue || - configuration.enableSiteCreationGuide - } - - // MARK: - Lifecycle Methods - - override func viewDidLoad() { - super.viewDidLoad() - - if let topContainerChildViewController = style.prologueTopContainerChildViewController() { - topContainerView.subviews.forEach { $0.removeFromSuperview() } - addChild(topContainerChildViewController) - topContainerView.addSubview(topContainerChildViewController.view) - topContainerChildViewController.didMove(toParent: self) - - topContainerChildViewController.view.translatesAutoresizingMaskIntoConstraints = false - topContainerView.pinSubviewToAllEdges(topContainerChildViewController.view) - } - - createButtonViewController() - - defaultButtonViewMargin = buttonViewLeadingConstraint?.constant ?? 0 - if let backgroundImage = WordPressAuthenticator.shared.unifiedStyle?.prologueBackgroundImage { - view.layer.contents = backgroundImage.cgImage - } - } - - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - configureButtonVC() - navigationController?.setNavigationBarHidden(true, animated: animated) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - // We've found some instances where the iCloud Keychain login flow was being started - // when the device was idle and the app was logged out and in the background. I couldn't - // find precise reproduction steps for this issue but my guess is that some background - // operation is triggering a call to this method while the app is in the background. - // The proposed solution is based off this StackOverflow reply: - // - // https://stackoverflow.com/questions/30584356/viewdidappear-is-called-when-app-is-started-due-to-significant-location-change - // - guard UIApplication.shared.applicationState != .background else { - return - } - - WordPressAuthenticator.track(.loginPrologueViewed) - - tracker.set(flow: .prologue) - - if !prologueFlowTracked { - tracker.track(step: .prologue) - prologueFlowTracked = true - } else { - tracker.set(step: .prologue) - } - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - self.navigationController?.setNavigationBarHidden(false, animated: animated) - } - - override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return UIDevice.isPad() ? .all : .portrait - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - setButtonViewMargins(forWidth: view.frame.width) - } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - setButtonViewMargins(forWidth: size.width) - } - - private func configureButtonVC() { - guard configuration.enableUnifiedAuth else { - buildPrologueButtons() - return - } - - if useStackedButtonsViewController { - buildPrologueButtonsUsingStackedButtonsViewController() - } else { - buildUnifiedPrologueButtons() - } - - if let buttonViewController { - buttonViewController.shadowLayoutGuide = view.safeAreaLayoutGuide - buttonViewController.topButtonStyle = WordPressAuthenticator.shared.style.prologuePrimaryButtonStyle - buttonViewController.bottomButtonStyle = WordPressAuthenticator.shared.style.prologueSecondaryButtonStyle - buttonViewController.tertiaryButtonStyle = WordPressAuthenticator.shared.style.prologueSecondaryButtonStyle - } else if let stackedButtonsViewController { - stackedButtonsViewController.shadowLayoutGuide = view.safeAreaLayoutGuide - } - } - - /// Displays the old UI prologue buttons. - /// - private func buildPrologueButtons() { - guard let buttonViewController else { - return - } - - let loginTitle = NSLocalizedString("Log In", comment: "Button title. Tapping takes the user to the login form.") - let createTitle = NSLocalizedString("Sign up for WordPress.com", comment: "Button title. Tapping begins the process of creating a WordPress.com account.") - - buttonViewController.setupTopButton(title: loginTitle, isPrimary: false, accessibilityIdentifier: "Prologue Log In Button") { [weak self] in - self?.onLoginButtonTapped?() - self?.loginTapped() - } - - if configuration.enableSignUp { - buttonViewController.setupBottomButton(title: createTitle, isPrimary: true, accessibilityIdentifier: "Prologue Signup Button") { [weak self] in - self?.signupTapped() - } - } - - if showCancel { - let cancelTitle = NSLocalizedString("Cancel", comment: "Button title. Tapping it cancels the login flow.") - buttonViewController.setupTertiaryButton(title: cancelTitle, isPrimary: false) { [weak self] in - self?.dismiss(animated: true, completion: nil) - } - } - - buttonViewController.backgroundColor = style.buttonViewBackgroundColor - buttonBlurEffectView.isHidden = true - } - - /// Displays the Unified prologue buttons. - /// - private func buildUnifiedPrologueButtons() { - guard let buttonViewController else { - return - } - - let displayStrings = WordPressAuthenticator.shared.displayStrings - let loginTitle = displayStrings.continueWithWPButtonTitle - let siteAddressTitle = displayStrings.enterYourSiteAddressButtonTitle - - if configuration.continueWithSiteAddressFirst { - buildUnifiedPrologueButtonsWithSiteAddressFirst(buttonViewController, loginTitle: loginTitle, siteAddressTitle: siteAddressTitle) - return - } - - buildDefaultUnifiedPrologueButtons(buttonViewController, loginTitle: loginTitle, siteAddressTitle: siteAddressTitle) - } - - private func buildDefaultUnifiedPrologueButtons(_ buttonViewController: NUXButtonViewController, loginTitle: String, siteAddressTitle: String) { - - setButtonViewMargins(forWidth: view.frame.width) - - buttonViewController.setupTopButton(title: loginTitle, isPrimary: true, configureBodyFontForTitle: true, accessibilityIdentifier: "Prologue Continue Button", onTap: loginTapCallback()) - - if configuration.enableUnifiedAuth { - buttonViewController.setupBottomButton(title: siteAddressTitle, isPrimary: false, configureBodyFontForTitle: true, accessibilityIdentifier: "Prologue Self Hosted Button", onTap: siteAddressTapCallback()) - } - - showCancelIfNeccessary(buttonViewController) - - setButtonViewControllerBackground() - } - - private func buildUnifiedPrologueButtonsWithSiteAddressFirst(_ buttonViewController: NUXButtonViewController, loginTitle: String, siteAddressTitle: String) { - guard configuration.enableUnifiedAuth == true else { - return - } - - setButtonViewMargins(forWidth: view.frame.width) - - buttonViewController.setupTopButton(title: siteAddressTitle, isPrimary: true, accessibilityIdentifier: "Prologue Self Hosted Button", onTap: siteAddressTapCallback()) - - buttonViewController.setupBottomButton(title: loginTitle, isPrimary: false, accessibilityIdentifier: "Prologue Continue Button", onTap: loginTapCallback()) - - showCancelIfNeccessary(buttonViewController) - - setButtonViewControllerBackground() - } - - private func buildPrologueButtonsUsingStackedButtonsViewController() { - guard let stackedButtonsViewController else { - return - } - - let primaryButtonStyle = WordPressAuthenticator.shared.style.prologuePrimaryButtonStyle - let secondaryButtonStyle = WordPressAuthenticator.shared.style.prologueSecondaryButtonStyle - - setButtonViewMargins(forWidth: view.frame.width) - let displayStrings = WordPressAuthenticator.shared.displayStrings - let buttons: [StackedButton] - - let continueWithWPButton: StackedButton? = { - guard !configuration.enableSiteAddressLoginOnlyInPrologue else { - return nil - } - return StackedButton(title: displayStrings.continueWithWPButtonTitle, - isPrimary: true, - configureBodyFontForTitle: true, - accessibilityIdentifier: "Prologue Continue Button", - style: primaryButtonStyle, - onTap: loginTapCallback()) - }() - - let enterYourSiteAddressButton: StackedButton? = { - guard !configuration.enableWPComLoginOnlyInPrologue else { - return nil - } - let isPrimary = configuration.enableSiteAddressLoginOnlyInPrologue && !configuration.enableSiteCreation - return StackedButton(title: displayStrings.enterYourSiteAddressButtonTitle, - isPrimary: isPrimary, - configureBodyFontForTitle: true, - accessibilityIdentifier: "Prologue Self Hosted Button", - style: secondaryButtonStyle, - onTap: siteAddressTapCallback()) - }() - - let createSiteButton: StackedButton? = { - guard configuration.enableSiteCreation else { - return nil - } - let isPrimary = configuration.enableSiteAddressLoginOnlyInPrologue - return StackedButton(title: displayStrings.siteCreationButtonTitle, - isPrimary: isPrimary, - configureBodyFontForTitle: true, - accessibilityIdentifier: "Prologue Create Site Button", - style: secondaryButtonStyle, - onTap: simplifiedLoginSiteCreationCallback()) - }() - - let createSiteButtonForBottomStackView: StackedButton? = { - guard let createSiteButton else { - return nil - } - return StackedButton(using: createSiteButton, stackView: .bottom) - }() - - let siteCreationGuideButton: StackedButton? = { - guard configuration.enableSiteCreationGuide else { - return nil - } - return StackedButton(title: displayStrings.siteCreationGuideButtonTitle, - isPrimary: false, - configureBodyFontForTitle: true, - accessibilityIdentifier: "Prologue Site Creation Guide button", - style: NUXButtonStyle.linkButtonStyle, - onTap: siteCreationGuideCallback()) - }() - - let showBothLoginOptions = continueWithWPButton != nil && enterYourSiteAddressButton != nil - buttons = [ - continueWithWPButton, - !showBothLoginOptions ? createSiteButton : nil, - enterYourSiteAddressButton, - showBothLoginOptions ? createSiteButtonForBottomStackView : nil, - siteCreationGuideButton - ].compactMap { $0 } - - let showDivider = configuration.enableWPComLoginOnlyInPrologue == false && - configuration.enableSiteCreation == true && - configuration.enableSiteAddressLoginOnlyInPrologue == false - stackedButtonsViewController.setUpButtons(using: buttons, showDivider: showDivider) - setButtonViewControllerBackground() - } - - private func siteAddressTapCallback() -> NUXButtonViewController.CallBackType { - return { [weak self] in - self?.siteAddressTapped() - } - } - - private func loginTapCallback() -> NUXButtonViewController.CallBackType { - return { [weak self] in - guard let self else { - return - } - - self.tracker.track(click: .continueWithWordPressCom) - self.continueWithDotCom() - } - } - - private func simplifiedLoginSiteCreationCallback() -> NUXButtonViewController.CallBackType { - { [weak self] in - guard let self, let navigationController = self.navigationController else { return } - // triggers the delegate to ask the host app to handle site creation - WordPressAuthenticator.shared.delegate?.showSiteCreation(in: navigationController) - } - } - - private func siteCreationGuideCallback() -> NUXButtonViewController.CallBackType { - { [weak self] in - guard let self, let navigationController else { return } - // triggers the delegate to ask the host app to handle site creation guide - WordPressAuthenticator.shared.delegate?.showSiteCreationGuide(in: navigationController) - } - } - - private func showCancelIfNeccessary(_ buttonViewController: NUXButtonViewController) { - if showCancel { - let cancelTitle = NSLocalizedString("Cancel", comment: "Button title. Tapping it cancels the login flow.") - buttonViewController.setupTertiaryButton(title: cancelTitle, isPrimary: false) { [weak self] in - self?.dismiss(animated: true, completion: nil) - } - } - } - - private func setButtonViewControllerBackground() { - // Fallback to setting the button background color to clear so the blur effect blurs the Prologue background color. - let buttonsBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.prologueButtonsBackgroundColor ?? .clear - buttonViewController?.backgroundColor = buttonsBackgroundColor - buttonBackgroundView?.backgroundColor = buttonsBackgroundColor - stackedButtonsViewController?.backgroundColor = buttonsBackgroundColor - - /// If host apps provide a background color for the prologue buttons: - /// 1. Hide the blur effect - /// 2. Set the background color of the view controller to prologueViewBackgroundColor - let prologueViewBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.prologueViewBackgroundColor ?? .clear - - guard prologueViewBackgroundColor.cgColor == buttonsBackgroundColor.cgColor else { - buttonBlurEffectView.effect = UIBlurEffect(style: blurEffect) - return - } - // do not set background color if we've set a background image earlier - if WordPressAuthenticator.shared.unifiedStyle?.prologueBackgroundImage == nil { - view.backgroundColor = prologueViewBackgroundColor - } - // if a blur effect for the buttons was passed, use it; otherwise hide the view. - guard let blurEffect = WordPressAuthenticator.shared.unifiedStyle?.prologueButtonsBlurEffect else { - buttonBlurEffectView.isHidden = true - return - } - buttonBlurEffectView.effect = blurEffect - } - - // MARK: - Actions - - /// Old UI. "Log In" button action. - /// - private func loginTapped() { - tracker.set(source: .default) - - guard let vc = LoginPrologueLoginMethodViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate to LoginPrologueLoginMethodViewController from LoginPrologueViewController") - return - } - - vc.transitioningDelegate = self - - // Continue with WordPress.com button action - vc.emailTapped = { [weak self] in - guard let self else { - return - } - - self.presentLoginEmailView() - } - - // Continue with Google button action - vc.googleTapped = { [weak self] in - self?.googleTapped() - } - - // Site address text link button action - vc.selfHostedTapped = { [weak self] in - self?.loginToSelfHostedSite() - } - - // Sign In With Apple (SIWA) button action - vc.appleTapped = { [weak self] in - self?.appleTapped() - } - - vc.modalPresentationStyle = .custom - navigationController?.present(vc, animated: true, completion: nil) - } - - /// Old UI. "Sign up with WordPress.com" button action. - /// - private func signupTapped() { - tracker.set(source: .default) - - // This stat is part of a funnel that provides critical information. - // Before making ANY modification to this stat please refer to: p4qSXL-35X-p2 - WordPressAuthenticator.track(.signupButtonTapped) - - guard let vc = LoginPrologueSignupMethodViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate to LoginPrologueSignupMethodViewController") - return - } - - vc.loginFields = self.loginFields - vc.dismissBlock = dismissBlock - vc.transitioningDelegate = self - vc.modalPresentationStyle = .custom - - vc.emailTapped = { [weak self] in - guard let self else { - return - } - - guard self.configuration.enableUnifiedAuth else { - self.presentSignUpEmailView() - return - } - - self.presentUnifiedSignupView() - } - - vc.googleTapped = { [weak self] in - guard let self else { - return - } - - guard self.configuration.enableUnifiedAuth else { - self.presentGoogleSignupView() - return - } - - self.presentUnifiedGoogleView() - } - - vc.appleTapped = { [weak self] in - self?.appleTapped() - } - - navigationController?.present(vc, animated: true, completion: nil) - } - - private func appleTapped() { - AppleAuthenticator.sharedInstance.delegate = self - AppleAuthenticator.sharedInstance.showFrom(viewController: self) - } - - private func googleTapped() { - guard configuration.enableUnifiedAuth else { - GoogleAuthenticator.sharedInstance.loginDelegate = self - GoogleAuthenticator.sharedInstance.showFrom(viewController: self, loginFields: loginFields, for: .login) - return - } - - presentUnifiedGoogleView() - } - - /// Unified "Continue with WordPress.com" prologue button action. - /// - private func continueWithDotCom() { - if let continueWithDotComOverwrite, continueWithDotComOverwrite(self) { - return - } - - guard let vc = GetStartedViewController.instantiate(from: .getStarted) else { - WPLogError("Failed to navigate from LoginPrologueViewController to GetStartedViewController") - return - } - vc.source = .wpCom - - navigationController?.pushViewController(vc, animated: true) - } - - /// Unified "Enter your existing site address" prologue button action. - /// - private func siteAddressTapped() { - tracker.track(click: .loginWithSiteAddress) - - if let selfHostedSiteLoginOverwrite, selfHostedSiteLoginOverwrite(self) { - return - } - - loginToSelfHostedSite() - } - - private func presentSignUpEmailView() { - guard let toVC = SignupEmailViewController.instantiate(from: .signup) else { - WPLogError("Failed to navigate to SignupEmailViewController") - return - } - - navigationController?.pushViewController(toVC, animated: true) - } - - private func presentUnifiedSignupView() { - guard let toVC = UnifiedSignupViewController.instantiate(from: .unifiedSignup) else { - WPLogError("Failed to navigate to UnifiedSignupViewController") - return - } - - navigationController?.pushViewController(toVC, animated: true) - } - - private func presentLoginEmailView() { - guard let toVC = LoginEmailViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate to LoginEmailVC from LoginPrologueVC") - return - } - - navigationController?.pushViewController(toVC, animated: true) - } - - // Shows the VC that handles both Google login & signup. - private func presentUnifiedGoogleView() { - guard let toVC = GoogleAuthViewController.instantiate(from: .googleAuth) else { - WPLogError("Failed to navigate to GoogleAuthViewController from LoginPrologueVC") - return - } - - navigationController?.pushViewController(toVC, animated: true) - } - - // Shows the VC that handles only Google signup. - private func presentGoogleSignupView() { - guard let toVC = SignupGoogleViewController.instantiate(from: .signup) else { - WPLogError("Failed to navigate to SignupGoogleViewController from LoginPrologueVC") - return - } - - navigationController?.pushViewController(toVC, animated: true) - } - - private func presentWPLogin() { - guard let vc = LoginWPComViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginPrologueViewController to LoginWPComViewController") - return - } - - vc.loginFields = self.loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - private func presentUnifiedPassword() { - guard let vc = PasswordViewController.instantiate(from: .password) else { - WPLogError("Failed to navigate from LoginPrologueViewController to PasswordViewController") - return - } - - vc.loginFields = loginFields - navigationController?.pushViewController(vc, animated: true) - } - - private func createButtonViewController() { - if useStackedButtonsViewController { - let stackedButtonsViewController = NUXStackedButtonsViewController.instance() - self.stackedButtonsViewController = stackedButtonsViewController - stackedButtonsViewController.move(to: self, into: buttonContainerView) - } else { - let buttonViewController = NUXButtonViewController.instance() - self.buttonViewController = buttonViewController - buttonViewController.move(to: self, into: buttonContainerView) - } - view.bringSubviewToFront(buttonContainerView) - } -} - -// MARK: - LoginFacadeDelegate - -extension LoginPrologueViewController { - - // Used by SIWA when logging with with a passwordless, 2FA account. - // - func needsMultifactorCode(forUserID userID: Int, andNonceInfo nonceInfo: SocialLogin2FANonceInfo) { - configureViewLoading(false) - socialNeedsMultifactorCode(forUserID: userID, andNonceInfo: nonceInfo) - } -} - -// MARK: - AppleAuthenticatorDelegate - -extension LoginPrologueViewController: AppleAuthenticatorDelegate { - - func showWPComLogin(loginFields: LoginFields) { - self.loginFields = loginFields - - guard WordPressAuthenticator.shared.configuration.enableUnifiedAuth else { - presentWPLogin() - return - } - - presentUnifiedPassword() - } - - func showApple2FA(loginFields: LoginFields) { - self.loginFields = loginFields - signInAppleAccount() - } - - func authFailedWithError(message: String) { - displayErrorAlert(message, sourceTag: .loginApple) - } -} - -// MARK: - GoogleAuthenticatorLoginDelegate - -extension LoginPrologueViewController: GoogleAuthenticatorLoginDelegate { - - func googleFinishedLogin(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - self.loginFields = loginFields - syncWPComAndPresentEpilogue(credentials: credentials) - } - - func googleNeedsMultifactorCode(loginFields: LoginFields) { - self.loginFields = loginFields - - guard let vc = Login2FAViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginViewController to Login2FAViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - func googleExistingUserNeedsConnection(loginFields: LoginFields) { - self.loginFields = loginFields - - guard let vc = LoginWPComViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from Google Login to LoginWPComViewController (password VC)") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - func googleLoginFailed(errorTitle: String, errorDescription: String, loginFields: LoginFields) { - self.loginFields = loginFields - - let socialErrorVC = LoginSocialErrorViewController(title: errorTitle, description: errorDescription) - let socialErrorNav = LoginNavigationController(rootViewController: socialErrorVC) - socialErrorVC.delegate = self - socialErrorVC.loginFields = loginFields - socialErrorVC.modalPresentationStyle = .fullScreen - present(socialErrorNav, animated: true) - } -} - -// MARK: - Button View Sizing - -private extension LoginPrologueViewController { - - /// Resize the button view based on trait collection. - /// Used only in unified views. - /// - func setButtonViewMargins(forWidth viewWidth: CGFloat) { - - guard configuration.enableUnifiedAuth else { - return - } - - guard traitCollection.horizontalSizeClass == .regular && - traitCollection.verticalSizeClass == .regular else { - buttonViewLeadingConstraint?.constant = defaultButtonViewMargin - buttonViewTrailingConstraint?.constant = defaultButtonViewMargin - return - } - - let marginMultiplier = UIDevice.current.orientation.isLandscape ? - ButtonViewMarginMultipliers.ipadLandscape : - ButtonViewMarginMultipliers.ipadPortrait - - let margin = viewWidth * marginMultiplier - - buttonViewLeadingConstraint?.constant = margin - buttonViewTrailingConstraint?.constant = margin - } - - private enum ButtonViewMarginMultipliers { - static let ipadPortrait: CGFloat = 0.1667 - static let ipadLandscape: CGFloat = 0.25 - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginSelfHostedViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginSelfHostedViewController.swift deleted file mode 100644 index 1ce4851a2c4c..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginSelfHostedViewController.swift +++ /dev/null @@ -1,281 +0,0 @@ -import UIKit -import WordPressShared - -/// Part two of the self-hosted sign in flow. Used by WPiOS and NiOS. -/// A valid site address should be acquired before presenting this view controller. -/// -class LoginSelfHostedViewController: LoginViewController, NUXKeyboardResponder { - @IBOutlet var siteHeaderView: SiteInfoHeaderView! - @IBOutlet var usernameField: WPWalkthroughTextField! - @IBOutlet var passwordField: WPWalkthroughTextField! - @IBOutlet var forgotPasswordButton: UIButton! - @IBOutlet var bottomContentConstraint: NSLayoutConstraint? - @IBOutlet var verticalCenterConstraint: NSLayoutConstraint? - override var sourceTag: WordPressSupportSourceTag { - get { - return .loginUsernamePassword - } - } - - override var loginFields: LoginFields { - didSet { - // Clear the username & password (if any) from LoginFields - loginFields.username = "" - loginFields.password = "" - } - } - - // MARK: - Lifecycle Methods - - override func viewDidLoad() { - super.viewDidLoad() - - configureHeader() - localizeControls() - displayLoginMessage("") - configureForAcessibility() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - // Update special case login fields. - loginFields.meta.userIsDotCom = false - - configureTextFields() - configureSubmitButton(animating: false) - configureViewForEditingIfNeeded() - - setupNavBarIcon() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - - WordPressAuthenticator.track(.loginUsernamePasswordFormViewed) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - } - - // MARK: - Setup and Configuration - - /// Assigns localized strings to various UIControl defined in the storyboard. - /// - @objc func localizeControls() { - usernameField.placeholder = NSLocalizedString("Username", comment: "Username placeholder") - passwordField.placeholder = NSLocalizedString("Password", comment: "Password placeholder") - - let submitButtonTitle = NSLocalizedString("Next", comment: "Title of a button. The text should be capitalized.").localizedCapitalized - submitButton?.setTitle(submitButtonTitle, for: .normal) - submitButton?.setTitle(submitButtonTitle, for: .highlighted) - - let forgotPasswordTitle = NSLocalizedString("Lost your password?", comment: "Title of a button. ") - forgotPasswordButton.setTitle(forgotPasswordTitle, for: .normal) - forgotPasswordButton.setTitle(forgotPasswordTitle, for: .highlighted) - forgotPasswordButton.titleLabel?.numberOfLines = 0 - } - - /// Sets up necessary accessibility labels and attributes for the all the UI elements in self. - /// - private func configureForAcessibility() { - usernameField.accessibilityLabel = - NSLocalizedString("Username", comment: "Accessibility label for the username text field in the self-hosted login page.") - passwordField.accessibilityLabel = - NSLocalizedString("Password", comment: "Accessibility label for the password text field in the self-hosted login page.") - - if UIAccessibility.isVoiceOverRunning { - // Remove the placeholder if VoiceOver is running. VoiceOver speaks the label and the - // placeholder together. In this case, both labels and placeholders are the same so it's - // like VoiceOver is reading the same thing twice. - usernameField.placeholder = nil - passwordField.placeholder = nil - } - - forgotPasswordButton.accessibilityTraits = .link - } - - /// Configures the content of the text fields based on what is saved in `loginFields`. - /// - @objc func configureTextFields() { - usernameField.text = loginFields.username - passwordField.text = loginFields.password - passwordField.contentInsets = WPStyleGuide.edgeInsetForLoginTextFields() - usernameField.contentInsets = WPStyleGuide.edgeInsetForLoginTextFields() - } - - /// Configures the appearance and state of the forgot password button. - /// - @objc func configureForgotPasswordButton() { - forgotPasswordButton.isEnabled = enableSubmit(animating: false) - WPStyleGuide.configureTextButton(forgotPasswordButton) - } - - /// Configures the appearance and state of the submit button. - /// - override func configureSubmitButton(animating: Bool) { - submitButton?.showActivityIndicator(animating) - - submitButton?.isEnabled = ( - !animating && - !loginFields.username.isEmpty && - !loginFields.password.isEmpty - ) - } - - /// Sets the view's state to loading or not loading. - /// - /// - Parameter loading: True if the form should be configured to a "loading" state. - /// - override func configureViewLoading(_ loading: Bool) { - usernameField.isEnabled = !loading - passwordField.isEnabled = !loading - - configureSubmitButton(animating: loading) - configureForgotPasswordButton() - navigationItem.hidesBackButton = loading - } - - /// Configure the view for an editing state. Should only be called from viewWillAppear - /// as this method skips animating any change in height. - /// - @objc func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editiing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - usernameField.becomeFirstResponder() - } - } - - /// Configure the site header. - /// - @objc func configureHeader() { - if let siteInfo = loginFields.meta.siteInfo { - configureBlogDetailHeaderView(siteInfo: siteInfo) - } else { - configureSiteAddressHeader() - } - } - - /// Configure the site header to show the BlogDetailsHeaderView - /// - func configureBlogDetailHeaderView(siteInfo: WordPressComSiteInfo) { - let siteAddress = sanitizedSiteAddress(siteAddress: siteInfo.url) - siteHeaderView.title = siteInfo.name - siteHeaderView.subtitle = NSURL.idnDecodedURL(siteAddress) - siteHeaderView.subtitleIsHidden = false - - siteHeaderView.blavatarBorderIsHidden = false - siteHeaderView.downloadBlavatar(at: siteInfo.icon) - } - - /// Configure the site header to show the site address label. - /// - @objc func configureSiteAddressHeader() { - siteHeaderView.title = sanitizedSiteAddress(siteAddress: loginFields.siteAddress) - siteHeaderView.subtitleIsHidden = true - - siteHeaderView.blavatarBorderIsHidden = true - siteHeaderView.blavatarImage = .linkFieldImage - } - - /// Sanitize and format the site address we show to users. - /// - @objc func sanitizedSiteAddress(siteAddress: String) -> String { - let baseSiteUrl = WordPressAuthenticator.baseSiteURL(string: siteAddress) as NSString - if let str = baseSiteUrl.components(separatedBy: "://").last { - return str - } - return siteAddress - } - - // MARK: - Instance Methods - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with the submit action. - /// - @objc func validateForm() { - validateFormAndLogin() - } - - // MARK: - Actions - - @IBAction func handleTextFieldDidChange(_ sender: UITextField) { - loginFields.username = usernameField.nonNilTrimmedText() - loginFields.password = passwordField.nonNilTrimmedText() - - configureForgotPasswordButton() - configureSubmitButton(animating: false) - } - - @IBAction func handleSubmitButtonTapped(_ sender: UIButton) { - validateForm() - } - - @IBAction func handleForgotPasswordButtonTapped(_ sender: UIButton) { - WordPressAuthenticator.openForgotPasswordURL(loginFields) - WordPressAuthenticator.track(.loginForgotPasswordClicked) - } - - // MARK: - Keyboard Notifications - - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } -} - -extension LoginSelfHostedViewController { - - func finishedLogin(withUsername username: String, password: String, xmlrpc: String, options: [AnyHashable: Any]) { - displayLoginMessage("") - - guard let delegate = WordPressAuthenticator.shared.delegate else { - fatalError() - } - - let wporg = WordPressOrgCredentials(username: username, password: password, xmlrpc: xmlrpc, options: options) - let credentials = AuthenticatorCredentials(wporg: wporg) - delegate.sync(credentials: credentials) { [weak self] in - - NotificationCenter.default.post(name: Foundation.Notification.Name(rawValue: WordPressAuthenticator.WPSigninDidFinishNotification), object: nil) - self?.showLoginEpilogue(for: credentials) - } - } - - func displayLoginMessage(_ message: String) { - configureForgotPasswordButton() - } - - override func displayRemoteError(_ error: Error) { - displayLoginMessage("") - configureViewLoading(false) - let err = error as NSError - if err.code == 403 { - let message = NSLocalizedString("It looks like this username/password isn't associated with this site.", - comment: "An error message shown during log in when the username or password is incorrect.") - displayError(message: message, moveVoiceOverFocus: true) - } else { - displayError(error, sourceTag: sourceTag) - } - } -} - -extension LoginSelfHostedViewController: UITextFieldDelegate { - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if textField == usernameField { - passwordField.becomeFirstResponder() - } else if textField == passwordField { - validateForm() - } - return true - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginSiteAddressViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginSiteAddressViewController.swift deleted file mode 100644 index 5936635c6db3..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginSiteAddressViewController.swift +++ /dev/null @@ -1,345 +0,0 @@ -import UIKit -import WordPressShared -import WordPressKit -import WordPressUI - -class LoginSiteAddressViewController: LoginViewController, NUXKeyboardResponder { - @IBOutlet weak var siteURLField: WPWalkthroughTextField! - @IBOutlet var siteAddressHelpButton: UIButton! - @IBOutlet var bottomContentConstraint: NSLayoutConstraint? - @IBOutlet var verticalCenterConstraint: NSLayoutConstraint? - override var sourceTag: WordPressSupportSourceTag { - get { - return .loginSiteAddress - } - } - - override var loginFields: LoginFields { - didSet { - // Clear the site url and site info (if any) from LoginFields - loginFields.siteAddress = "" - loginFields.meta.siteInfo = nil - } - } - - // MARK: - URL Validation - - private lazy var urlErrorDebouncer = Debouncer(delay: 2) { [weak self] in - let errorMessage = NSLocalizedString("Please enter a complete website address, like example.com.", comment: "Error message shown when a URL is invalid.") - - self?.displayError(message: errorMessage) - } - - // MARK: - Lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - localizeControls() - configureForAccessibility() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - // Update special case login fields. - loginFields.meta.userIsDotCom = false - - configureTextFields() - configureSiteAddressHelpButton() - configureSubmitButton(animating: false) - configureViewForEditingIfNeeded() - - navigationController?.setNavigationBarHidden(false, animated: false) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - WordPressAuthenticator.track(.loginURLFormViewed) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - } - - // MARK: Setup and Configuration - - /// Assigns localized strings to various UIControl defined in the storyboard. - /// - @objc func localizeControls() { - instructionLabel?.text = WordPressAuthenticator.shared.displayStrings.siteLoginInstructions - - siteURLField.placeholder = NSLocalizedString("example.com", comment: "Site Address placeholder") - - let submitButtonTitle = NSLocalizedString("Next", comment: "Title of a button. The text should be capitalized.").localizedCapitalized - submitButton?.setTitle(submitButtonTitle, for: .normal) - submitButton?.setTitle(submitButtonTitle, for: .highlighted) - submitButton?.accessibilityIdentifier = "Site Address Next Button" - - let siteAddressHelpTitle = NSLocalizedString("Need help finding your site address?", comment: "A button title.") - siteAddressHelpButton.setTitle(siteAddressHelpTitle, for: .normal) - siteAddressHelpButton.setTitle(siteAddressHelpTitle, for: .highlighted) - siteAddressHelpButton.titleLabel?.numberOfLines = 0 - } - - /// Sets up necessary accessibility labels and attributes for the all the UI elements in self. - /// - private func configureForAccessibility() { - siteURLField.accessibilityLabel = - NSLocalizedString("Site address", comment: "Accessibility label of the site address field shown when adding a self-hosted site.") - } - - /// Configures the content of the text fields based on what is saved in `loginFields`. - /// - @objc func configureTextFields() { - siteURLField.contentInsets = WPStyleGuide.edgeInsetForLoginTextFields() - siteURLField.text = loginFields.siteAddress - } - - /// Configures the appearance and state of the submit button. - /// - override func configureSubmitButton(animating: Bool) { - submitButton?.showActivityIndicator(animating) - - submitButton?.isEnabled = ( - !animating && canSubmit() - ) - } - - /// Sets the view's state to loading or not loading. - /// - /// - Parameter loading: True if the form should be configured to a "loading" state. - /// - override func configureViewLoading(_ loading: Bool) { - siteURLField.isEnabled = !loading - - configureSubmitButton(animating: loading) - navigationItem.hidesBackButton = loading - } - - /// Configure the view for an editing state. Should only be called from viewWillAppear - /// as this method skips animating any change in height. - /// - @objc func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - siteURLField.becomeFirstResponder() - } - } - - private func configureSiteAddressHelpButton() { - WPStyleGuide.configureTextButton(siteAddressHelpButton) - } - - // MARK: - Instance Methods - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with the submit action. - /// - @objc func validateForm() { - view.endEditing(true) - displayError(message: "") - - // We need to to this here because before this point we need the URL to be pre-validated - // exactly as the user inputs it, and after this point we need the URL to be the base site URL. - // This isn't really great, but it's the only sane solution I could come up with given the current - // architecture of this pod. - loginFields.siteAddress = WordPressAuthenticator.baseSiteURL(string: loginFields.siteAddress) - - configureViewLoading(true) - - let facade = WordPressXMLRPCAPIFacade() - facade.guessXMLRPCURL(forSite: loginFields.siteAddress, success: { [weak self] url in - // Success! We now know that we have a valid XML-RPC endpoint. - // At this point, we do NOT know if this is a WP.com site or a self-hosted site. - if let url { - self?.loginFields.meta.xmlrpcURL = url as NSURL - } - // Let's try to grab site info in preparation for the next screen. - self?.fetchSiteInfo() - }, failure: { [weak self] error in - guard let error, let self else { - return - } - WPLogError(error.localizedDescription) - WordPressAuthenticator.track(.loginFailedToGuessXMLRPC, error: error) - WordPressAuthenticator.track(.loginFailed, error: error) - self.configureViewLoading(false) - - let err = self.originalErrorOrError(error: error as NSError) - - if let xmlrpcValidatorError = err as? WordPressOrgXMLRPCValidatorError { - self.displayError(message: xmlrpcValidatorError.localizedDescription, moveVoiceOverFocus: true) - } else if (err.domain == NSURLErrorDomain && err.code == NSURLErrorCannotFindHost) || - (err.domain == NSURLErrorDomain && err.code == NSURLErrorNetworkConnectionLost) { - // NSURLErrorNetworkConnectionLost can be returned when an invalid URL is entered. - let msg = NSLocalizedString( - "The site at this address is not a WordPress site. For us to connect to it, the site must use WordPress.", - comment: "Error message shown a URL does not point to an existing site.") - self.displayError(message: msg, moveVoiceOverFocus: true) - } else { - self.displayError(error, sourceTag: self.sourceTag) - } - }) - } - - @objc func fetchSiteInfo() { - let baseSiteUrl = WordPressAuthenticator.baseSiteURL(string: loginFields.siteAddress) - let service = WordPressComBlogService() - let successBlock: (WordPressComSiteInfo) -> Void = { [weak self] siteInfo in - guard let self else { - return - } - self.configureViewLoading(false) - if siteInfo.isWPCom && WordPressAuthenticator.shared.delegate?.allowWPComLogin == false { - // Hey, you have to log out of your existing WP.com account before logging into another one. - self.promptUserToLogoutBeforeConnectingWPComSite() - return - } - self.presentNextControllerIfPossible(siteInfo: siteInfo) - } - service.fetchUnauthenticatedSiteInfoForAddress(for: baseSiteUrl, success: successBlock, failure: { [weak self] _ in - self?.configureViewLoading(false) - guard let self else { - return - } - self.presentNextControllerIfPossible(siteInfo: nil) - }) - } - - func presentNextControllerIfPossible(siteInfo: WordPressComSiteInfo?) { - WordPressAuthenticator.shared.delegate?.shouldPresentUsernamePasswordController(for: siteInfo, onCompletion: { result in - switch result { - case let .error(error): - self.displayError(message: error.localizedDescription) - case let .presentPasswordController(isSelfHosted): - if isSelfHosted { - self.showSelfHostedUsernamePassword() - } - - self.showWPUsernamePassword() - case .presentEmailController: - // This case is only used for UL&S - break - case .injectViewController: - // This case is only used for UL&S - break - } - }) - } - - @objc func originalErrorOrError(error: NSError) -> NSError { - guard let err = error.userInfo[XMLRPCOriginalErrorKey] as? NSError else { - return error - } - return err - } - - /// Here we will continue with the self-hosted flow. - /// - @objc func showSelfHostedUsernamePassword() { - configureViewLoading(false) - guard let vc = LoginSelfHostedViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginEmailViewController to LoginSelfHostedViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - /// Break away from the self-hosted flow. - /// Display a username / password login screen for WP.com sites. - /// - @objc func showWPUsernamePassword() { - configureViewLoading(false) - - guard let vc = LoginUsernamePasswordViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginSiteAddressViewController to LoginUsernamePasswordViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - /// Whether the form can be submitted. - /// - @objc func canSubmit() -> Bool { - return loginFields.validateSiteForSignin() - } - - @objc private func promptUserToLogoutBeforeConnectingWPComSite() { - let acceptActionTitle = NSLocalizedString("OK", comment: "Alert dismissal title") - let message = NSLocalizedString("Please log out before connecting to a different wordpress.com site", comment: "Message for alert to prompt user to logout before connecting to a different wordpress.com site.") - let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) - alertController.addDefaultActionWithTitle(acceptActionTitle) - present(alertController, animated: true) - } - - // MARK: - URL Validation - - /// Does a local / quick Site Address validation and refreshes the UI with an error - /// if necessary. - /// - /// - Returns: `true` if the Site Address contains a valid URL. `false` otherwise. - /// - private func refreshSiteAddressError(immediate: Bool) { - let showError = !loginFields.siteAddress.isEmpty && !loginFields.validateSiteForSignin() - - if showError { - urlErrorDebouncer.call(immediate: immediate) - } else { - urlErrorDebouncer.cancel() - displayError(message: "") - } - } - - // MARK: - Actions - - @IBAction func handleSubmitForm() { - if canSubmit() { - validateForm() - } - } - - @IBAction func handleSubmitButtonTapped(_ sender: UIButton) { - validateForm() - } - - @IBAction func handleSiteAddressHelpButtonTapped(_ sender: UIButton) { - SiteAddressViewController.showSiteAddressHelpAlert(from: self, sourceTag: sourceTag) - WordPressAuthenticator.track(.loginURLHelpScreenViewed) - } - - @IBAction func handleTextFieldDidChange(_ sender: UITextField) { - displayError(message: "") - loginFields.siteAddress = siteURLField.nonNilTrimmedText() - configureSubmitButton(animating: false) - refreshSiteAddressError(immediate: false) - } - - @IBAction func handleEditingDidEnd(_ sender: UITextField) { - refreshSiteAddressError(immediate: true) - } - - // MARK: - Keyboard Notifications - - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginSocialErrorCell.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginSocialErrorCell.swift deleted file mode 100644 index b9f02c30ef4d..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginSocialErrorCell.swift +++ /dev/null @@ -1,94 +0,0 @@ -import WordPressShared - -open class LoginSocialErrorCell: UITableViewCell { - private let errorTitle: String - private let errorDescription: String - private var errorDescriptionStyled: NSAttributedString? - private let titleLabel: UILabel - private let descriptionLabel: UILabel - private let labelStack: UIStackView - - private var forUnified: Bool = false - - private struct Constants { - static let labelSpacing: CGFloat = 15.0 - static let labelVerticalMargin: CGFloat = 20.0 - static let descriptionMinHeight: CGFloat = 14.0 - } - - @objc public init(title: String, description: String, forUnified: Bool = false) { - errorTitle = title - errorDescription = description - titleLabel = UILabel() - descriptionLabel = UILabel() - labelStack = UIStackView() - self.forUnified = forUnified - - super.init(style: .default, reuseIdentifier: "LoginSocialErrorCell") - - layoutLabels() - } - - public init(title: String, description styledDescription: NSAttributedString) { - errorDescriptionStyled = styledDescription - errorDescription = "" - errorTitle = title - titleLabel = UILabel() - descriptionLabel = UILabel() - labelStack = UIStackView() - - super.init(style: .default, reuseIdentifier: "LoginSocialErrorCell") - - layoutLabels() - } - - required public init?(coder aDecoder: NSCoder) { - errorTitle = aDecoder.value(forKey: "errorTitle") as? String ?? "" - errorDescription = aDecoder.value(forKey: "errorDescription") as? String ?? "" - titleLabel = UILabel() - descriptionLabel = UILabel() - labelStack = UIStackView() - - super.init(coder: aDecoder) - - layoutLabels() - } - - private func layoutLabels() { - contentView.addSubview(labelStack) - labelStack.translatesAutoresizingMaskIntoConstraints = false - labelStack.addArrangedSubview(titleLabel) - labelStack.addArrangedSubview(descriptionLabel) - labelStack.axis = .vertical - labelStack.spacing = Constants.labelSpacing - - let style = WordPressAuthenticator.shared.style - titleLabel.font = WPStyleGuide.fontForTextStyle(.footnote) - titleLabel.textColor = style.instructionColor - descriptionLabel.font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - descriptionLabel.textColor = style.subheadlineColor - descriptionLabel.numberOfLines = 0 - descriptionLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: Constants.descriptionMinHeight).isActive = true - - contentView.addConstraints([ - contentView.topAnchor.constraint(equalTo: labelStack.topAnchor, constant: Constants.labelVerticalMargin * -1.0), - contentView.bottomAnchor.constraint(equalTo: labelStack.bottomAnchor, constant: Constants.labelVerticalMargin), - contentView.layoutMarginsGuide.leadingAnchor.constraint(equalTo: labelStack.leadingAnchor), - contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: labelStack.trailingAnchor) - ]) - - titleLabel.text = errorTitle.localizedUppercase - if let styledDescription = errorDescriptionStyled { - descriptionLabel.attributedText = styledDescription - } else { - descriptionLabel.text = errorDescription - } - - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - backgroundColor = WordPressAuthenticator.shared.style.viewControllerBackgroundColor - return - } - - backgroundColor = forUnified ? unifiedBackgroundColor : WordPressAuthenticator.shared.style.viewControllerBackgroundColor - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginSocialErrorViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginSocialErrorViewController.swift deleted file mode 100644 index 9c7c9c25250b..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginSocialErrorViewController.swift +++ /dev/null @@ -1,217 +0,0 @@ -import Foundation -import Gridicons -import WordPressShared - -@objc -protocol LoginSocialErrorViewControllerDelegate { - func retryWithEmail() - func retryWithAddress() - func retryAsSignup() - func errorDismissed() -} - -/// ViewController for presenting recovery options when social login fails -class LoginSocialErrorViewController: NUXTableViewController { - fileprivate var errorTitle: String - fileprivate var errorDescription: String - @objc weak var delegate: LoginSocialErrorViewControllerDelegate? - - private var forUnified: Bool = false - private var actionButtonTapped: Bool = false - private let unifiedAuthEnabled = WordPressAuthenticator.shared.configuration.enableUnifiedAuth - - fileprivate enum Sections: Int { - case titleAndDescription = 0 - case buttons = 1 - - static var count: Int { - return buttons.rawValue + 1 - } - } - - fileprivate enum Buttons: Int { - case tryEmail = 0 - case tryAddress = 1 - case signup = 2 - - static var count: Int { - return signup.rawValue + 1 - } - } - - /// Create and instance of LoginSocialErrorViewController - /// - /// - Parameters: - /// - title: The title that will be shown on the error VC - /// - description: A brief explination of what failed during social login - @objc init(title: String, description: String) { - errorTitle = title - errorDescription = description - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - errorTitle = aDecoder.value(forKey: "errorTitle") as? String ?? "" - errorDescription = aDecoder.value(forKey: "errorDescription") as? String ?? "" - - super.init(coder: aDecoder) - } - - override func viewDidLoad() { - super.viewDidLoad() - - let unifiedGoogle = unifiedAuthEnabled && loginFields.meta.socialService == .google - let unifiedApple = unifiedAuthEnabled && loginFields.meta.socialService == .apple - forUnified = unifiedGoogle || unifiedApple - - styleBackground() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - if !actionButtonTapped { - delegate?.errorDismissed() - } - } - - private func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - view.backgroundColor = WordPressAuthenticator.shared.style.viewControllerBackgroundColor - return - } - - view.backgroundColor = forUnified ? unifiedBackgroundColor : WordPressAuthenticator.shared.style.viewControllerBackgroundColor - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard indexPath.section == Sections.buttons.rawValue, - let delegate else { - return - } - - actionButtonTapped = true - - switch indexPath.row { - case Buttons.tryEmail.rawValue: - delegate.retryWithEmail() - case Buttons.tryAddress.rawValue: - if loginFields.restrictToWPCom { - fallthrough - } else { - delegate.retryWithAddress() - } - case Buttons.signup.rawValue: - fallthrough - default: - delegate.retryAsSignup() - } - } -} - -// MARK: UITableViewDelegate methods - -extension LoginSocialErrorViewController { - private struct RowHeightConstants { - static let estimate: CGFloat = 45.0 - static let automatic: CGFloat = UITableView.automaticDimension - } - - override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - return RowHeightConstants.estimate - } - - override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return RowHeightConstants.automatic - } -} - -// MARK: UITableViewDataSource methods - -extension LoginSocialErrorViewController { - private func numberOfButtonsToShow() -> Int { - - var buttonCount = loginFields.restrictToWPCom ? Buttons.count - 1 : Buttons.count - - // Don't show the Signup Retry if showing unified social flows. - // At this point, we've already tried signup and are past it. - let unifiedGoogle = unifiedAuthEnabled && loginFields.meta.socialService == .google - let unifiedApple = unifiedAuthEnabled && loginFields.meta.socialService == .apple - - if unifiedGoogle || unifiedApple { - buttonCount -= 1 - } - - return buttonCount - } - - override func numberOfSections(in tableView: UITableView) -> Int { - return Sections.count - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - switch section { - case Sections.titleAndDescription.rawValue: - return 1 - case Sections.buttons.rawValue: - return numberOfButtonsToShow() - default: - return 0 - } - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell: UITableViewCell - switch indexPath.section { - case Sections.titleAndDescription.rawValue: - cell = titleAndDescriptionCell() - case Sections.buttons.rawValue: - fallthrough - default: - cell = buttonCell(index: indexPath.row) - } - return cell - } - - override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - let footer = UIView() - footer.backgroundColor = WordPressAuthenticator.shared.style.viewControllerBackgroundColor - return footer - } - - override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - return 0.5 - } - - private func titleAndDescriptionCell() -> UITableViewCell { - return LoginSocialErrorCell(title: errorTitle, description: errorDescription, forUnified: forUnified) - } - - private func buttonCell(index: Int) -> UITableViewCell { - let cell = UITableViewCell() - let buttonText: String - let buttonIcon: UIImage - switch index { - case Buttons.tryEmail.rawValue: - buttonText = NSLocalizedString("Try with another email", comment: "When social login fails, this button offers to let the user try again with a differen email address") - buttonIcon = .gridicon(.undo) - case Buttons.tryAddress.rawValue: - if loginFields.restrictToWPCom { - fallthrough - } else { - buttonText = NSLocalizedString("Try with the site address", comment: "When social login fails, this button offers to let them try tp login using a URL") - buttonIcon = .gridicon(.domains) - } - case Buttons.signup.rawValue: - fallthrough - default: - buttonText = NSLocalizedString("Sign up", comment: "When social login fails, this button offers to let them signup for a new WordPress.com account") - buttonIcon = .gridicon(.mySites) - } - cell.textLabel?.text = buttonText - cell.textLabel?.textColor = WPStyleGuide.darkGrey() - cell.imageView?.image = buttonIcon.imageWithTintColor(WPStyleGuide.grey()) - return cell - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginUsernamePasswordViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginUsernamePasswordViewController.swift deleted file mode 100644 index 63afb958adfa..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginUsernamePasswordViewController.swift +++ /dev/null @@ -1,245 +0,0 @@ -import UIKit -import WordPressShared - -/// Part two of the self-hosted sign in flow. For use by WCiOS only. -/// A valid site address should be acquired before presenting this view controller. -/// -class LoginUsernamePasswordViewController: LoginViewController, NUXKeyboardResponder { - @IBOutlet var siteHeaderView: SiteInfoHeaderView! - @IBOutlet var usernameField: WPWalkthroughTextField! - @IBOutlet var passwordField: WPWalkthroughTextField! - @IBOutlet var forgotPasswordButton: UIButton! - @IBOutlet var bottomContentConstraint: NSLayoutConstraint? - @IBOutlet var verticalCenterConstraint: NSLayoutConstraint? - override var sourceTag: WordPressSupportSourceTag { - get { - return .loginWPComUsernamePassword - } - } - - override var loginFields: LoginFields { - didSet { - // Clear the username & password (if any) from LoginFields - loginFields.username = "" - loginFields.password = "" - } - } - - // MARK: - Lifecycle Methods - - override func viewDidLoad() { - super.viewDidLoad() - - configureHeader() - localizeControls() - displayLoginMessage("") - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - // Update special case login fields. - loginFields.meta.userIsDotCom = true - - configureTextFields() - configureSubmitButton(animating: false) - configureViewForEditingIfNeeded() - - setupNavBarIcon() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - - WordPressAuthenticator.track(.loginUsernamePasswordFormViewed) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - } - - // MARK: - Setup and Configuration - - /// Assigns localized strings to various UIControl defined in the storyboard. - /// - @objc func localizeControls() { - instructionLabel?.text = WordPressAuthenticator.shared.displayStrings.usernamePasswordInstructions - - usernameField.placeholder = NSLocalizedString("Username", comment: "Username placeholder") - passwordField.placeholder = NSLocalizedString("Password", comment: "Password placeholder") - - let submitButtonTitle = NSLocalizedString("Next", comment: "Title of a button. The text should be capitalized.").localizedCapitalized - submitButton?.setTitle(submitButtonTitle, for: .normal) - submitButton?.setTitle(submitButtonTitle, for: .highlighted) - - let forgotPasswordTitle = NSLocalizedString("Lost your password?", comment: "Title of a button. ") - forgotPasswordButton.setTitle(forgotPasswordTitle, for: .normal) - forgotPasswordButton.setTitle(forgotPasswordTitle, for: .highlighted) - forgotPasswordButton.titleLabel?.numberOfLines = 0 - } - - /// Configures the content of the text fields based on what is saved in `loginFields`. - /// - @objc func configureTextFields() { - usernameField.text = loginFields.username - passwordField.text = loginFields.password - passwordField.contentInsets = WPStyleGuide.edgeInsetForLoginTextFields() - usernameField.contentInsets = WPStyleGuide.edgeInsetForLoginTextFields() - } - - /// Configures the appearance and state of the forgot password button. - /// - @objc func configureForgotPasswordButton() { - forgotPasswordButton.isEnabled = enableSubmit(animating: false) - WPStyleGuide.configureTextButton(forgotPasswordButton) - } - - /// Configures the appearance and state of the submit button. - /// - override func configureSubmitButton(animating: Bool) { - submitButton?.showActivityIndicator(animating) - - submitButton?.isEnabled = ( - !animating && - !loginFields.username.isEmpty && - !loginFields.password.isEmpty - ) - } - - /// Sets the view's state to loading or not loading. - /// - /// - Parameter loading: True if the form should be configured to a "loading" state. - /// - override func configureViewLoading(_ loading: Bool) { - usernameField.isEnabled = !loading - passwordField.isEnabled = !loading - - configureSubmitButton(animating: loading) - configureForgotPasswordButton() - navigationItem.hidesBackButton = loading - } - - /// Configure the view for an editing state. Should only be called from viewWillAppear - /// as this method skips animating any change in height. - /// - @objc func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editiing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - usernameField.becomeFirstResponder() - } - } - - /// Configure the site header. - /// - @objc func configureHeader() { - if let siteInfo = loginFields.meta.siteInfo { - configureBlogDetailHeaderView(siteInfo: siteInfo) - } else { - configureSiteAddressHeader() - } - } - - /// Configure the site header to show the BlogDetailsHeaderView - /// - func configureBlogDetailHeaderView(siteInfo: WordPressComSiteInfo) { - let siteAddress = sanitizedSiteAddress(siteAddress: siteInfo.url) - siteHeaderView.title = siteInfo.name - siteHeaderView.subtitle = NSURL.idnDecodedURL(siteAddress) - siteHeaderView.subtitleIsHidden = false - - siteHeaderView.blavatarBorderIsHidden = false - siteHeaderView.downloadBlavatar(at: siteInfo.icon) - } - - /// Configure the site header to show the site address label. - /// - @objc func configureSiteAddressHeader() { - siteHeaderView.title = sanitizedSiteAddress(siteAddress: loginFields.siteAddress) - siteHeaderView.subtitleIsHidden = true - - siteHeaderView.blavatarBorderIsHidden = true - siteHeaderView.blavatarImage = .linkFieldImage - } - - /// Sanitize and format the site address we show to users. - /// - @objc func sanitizedSiteAddress(siteAddress: String) -> String { - let baseSiteUrl = WordPressAuthenticator.baseSiteURL(string: siteAddress) as NSString - if let str = baseSiteUrl.components(separatedBy: "://").last { - return str - } - return siteAddress - } - - // MARK: - Instance Methods - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with the submit action. - /// - @objc func validateForm() { - validateFormAndLogin() - } - - // MARK: - Actions - - @IBAction func handleTextFieldDidChange(_ sender: UITextField) { - loginFields.username = usernameField.nonNilTrimmedText() - loginFields.password = passwordField.nonNilTrimmedText() - - configureForgotPasswordButton() - configureSubmitButton(animating: false) - } - - @IBAction func handleSubmitButtonTapped(_ sender: UIButton) { - validateForm() - } - - @IBAction func handleForgotPasswordButtonTapped(_ sender: UIButton) { - WordPressAuthenticator.openForgotPasswordURL(loginFields) - WordPressAuthenticator.track(.loginForgotPasswordClicked) - } - - // MARK: - Keyboard Notifications - - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } -} - -extension LoginUsernamePasswordViewController { - - func displayLoginMessage(_ message: String) { - configureForgotPasswordButton() - } - - override func displayRemoteError(_ error: Error) { - displayLoginMessage("") - configureViewLoading(false) - let err = error as NSError - if err.code == 403 { - displayError(message: NSLocalizedString("It looks like this username/password isn't associated with this site.", comment: "An error message shown during log in when the username or password is incorrect.")) - } else { - displayError(error, sourceTag: sourceTag) - } - } -} - -extension LoginUsernamePasswordViewController: UITextFieldDelegate { - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if textField == usernameField { - passwordField.becomeFirstResponder() - } else if textField == passwordField { - validateForm() - } - return true - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginViewController.swift deleted file mode 100644 index a94e0aa98bda..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginViewController.swift +++ /dev/null @@ -1,539 +0,0 @@ -import WordPressShared -import WordPressKit - -/// View Controller for login-specific screens -open class LoginViewController: NUXViewController, LoginFacadeDelegate { - @IBOutlet var instructionLabel: UILabel? - @objc var errorToPresent: Error? - - let tracker = AuthenticatorAnalyticsTracker.shared - - /// Constraints on the table view container. - /// Used to adjust the table width in unified views. - @IBOutlet var tableViewLeadingConstraint: NSLayoutConstraint? - @IBOutlet var tableViewTrailingConstraint: NSLayoutConstraint? - var defaultTableViewMargin: CGFloat = 0 - - lazy var loginFacade: LoginFacade = { - let configuration = WordPressAuthenticator.shared.configuration - let facade = LoginFacade(dotcomClientID: configuration.wpcomClientId, - dotcomSecret: configuration.wpcomSecret, - userAgent: configuration.userAgent) - facade.delegate = self - return facade - }() - - var isJetpackLogin: Bool { - return loginFields.meta.jetpackLogin - } - - private var isSignUp: Bool { - return loginFields.meta.emailMagicLinkSource == .signup - } - - var authenticationDelegate: WordPressAuthenticatorDelegate { - guard let delegate = WordPressAuthenticator.shared.delegate else { - fatalError() - } - - return delegate - } - - open override var preferredStatusBarStyle: UIStatusBarStyle { - // Set to the old style as the default. - // Each VC in the unified flows needs to override this to use the unified style. - return WordPressAuthenticator.shared.style.statusBarStyle - } - - // MARK: Lifecycle Methods - - override open func viewDidLoad() { - super.viewDidLoad() - - displayError(message: "") - styleBackground() - styleInstructions() - - if let error = errorToPresent { - displayRemoteError(error) - errorToPresent = nil - } - } - - override open func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - if isBeingDismissedInAnyWay { - tracker.track(click: .dismiss) - } - } - - func didChangePreferredContentSize() { - styleInstructions() - } - - // MARK: - Setup and Configuration - - /// Styles the view's background color. Defaults to WPStyleGuide.lightGrey() - /// - func styleBackground() { - view.backgroundColor = WordPressAuthenticator.shared.style.viewControllerBackgroundColor - } - - /// Configures instruction label font - /// - func styleInstructions() { - instructionLabel?.font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - instructionLabel?.adjustsFontForContentSizeCategory = true - instructionLabel?.textColor = WordPressAuthenticator.shared.style.instructionColor - } - - func configureViewLoading(_ loading: Bool) { - configureSubmitButton(animating: loading) - navigationItem.hidesBackButton = loading - } - - /// Sets the text of the error label. - /// - /// - Parameter message: The message to display in the `errorLabel`. If empty, the `errorLabel` - /// will be hidden. - /// - Parameter moveVoiceOverFocus: If `true`, moves the VoiceOver focus to the `errorLabel`. - /// You will want to set this to `true` if the error was caused after pressing a button - /// (e.g. Next button). - func displayError(message: String, moveVoiceOverFocus: Bool = false) { - guard !message.isEmpty else { - errorLabel?.isHidden = true - return - } - - tracker.track(failure: message) - - errorLabel?.isHidden = false - errorLabel?.text = message - errorToPresent = nil - - if moveVoiceOverFocus, let errorLabel { - UIAccessibility.post(notification: .layoutChanged, argument: errorLabel) - } - } - - private func mustShowLoginEpilogue() -> Bool { - return isSignUp == false && authenticationDelegate.shouldPresentLoginEpilogue(isJetpackLogin: isJetpackLogin) - } - - private func mustShowSignupEpilogue() -> Bool { - return isSignUp && authenticationDelegate.shouldPresentSignupEpilogue() - } - - // MARK: - Epilogue - - func showSignupEpilogue(for credentials: AuthenticatorCredentials) { - guard let navigationController else { - fatalError() - } - - authenticationDelegate.presentSignupEpilogue( - in: navigationController, - for: credentials, - socialUser: loginFields.meta.socialUser - ) - } - - func showLoginEpilogue(for credentials: AuthenticatorCredentials) { - guard let navigationController else { - fatalError() - } - - authenticationDelegate.presentLoginEpilogue(in: navigationController, - for: credentials, - source: WordPressAuthenticator.shared.signInSource) { [weak self] in - self?.dismissBlock?(false) - } - } - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with login. - /// - func validateFormAndLogin() { - view.endEditing(true) - displayError(message: "") - - // Is everything filled out? - if !loginFields.validateFieldsPopulatedForSignin() { - let errorMsg = LocalizedText.missingInfoError - displayError(message: errorMsg) - - return - } - - configureViewLoading(true) - - loginFacade.signIn(with: loginFields) - } - - // MARK: SigninWPComSyncHandler methods - dynamic open func finishedLogin(withAuthToken authToken: String, requiredMultifactorCode: Bool) { - let wpcom = WordPressComCredentials(authToken: authToken, isJetpackLogin: isJetpackLogin, multifactor: requiredMultifactorCode, siteURL: loginFields.siteAddress) - let credentials = AuthenticatorCredentials(wpcom: wpcom) - - syncWPComAndPresentEpilogue(credentials: credentials) - - linkSocialServiceIfNeeded(loginFields: loginFields, wpcomAuthToken: authToken) - } - - func configureStatusLabel(_ message: String) { - // this is now a no-op, unless status labels return - } - - /// Overridden here to direct these errors to the login screen's error label - dynamic open func displayRemoteError(_ error: Error) { - configureViewLoading(false) - let err = error as NSError - guard err.code != 403 else { - let message = LocalizedText.loginError - displayError(message: message) - return - } - - displayError(err, sourceTag: sourceTag) - } - - open func needsMultifactorCode() { - displayError(message: "") - configureViewLoading(false) - - if tracker.shouldUseLegacyTracker() { - WordPressAuthenticator.track(.twoFactorCodeRequested) - } - - let unifiedAuthEnabled = WordPressAuthenticator.shared.configuration.enableUnifiedAuth - let unifiedGoogle = unifiedAuthEnabled && loginFields.meta.socialService == .google - let unifiedApple = unifiedAuthEnabled && loginFields.meta.socialService == .apple - let unifiedSiteAddress = unifiedAuthEnabled && !loginFields.siteAddress.isEmpty - let unifiedWordPress = unifiedAuthEnabled && loginFields.meta.userIsDotCom - - guard unifiedGoogle || unifiedApple || unifiedSiteAddress || unifiedWordPress else { - presentLogin2FA() - return - } - - // Make sure we don't provide any old nonce information when we are required to present only the multi-factor code option. - loginFields.nonceInfo = nil - loginFields.nonceUserID = 0 - - presentUnified2FA() - } - - private enum LocalizedText { - static let loginError = NSLocalizedString("Whoops, something went wrong and we couldn't log you in. Please try again!", comment: "An error message shown when a wpcom user provides the wrong password.") - static let missingInfoError = NSLocalizedString("Please fill out all the fields", comment: "A short prompt asking the user to properly fill out all login fields.") - static let gettingAccountInfo = NSLocalizedString("Getting account information", comment: "Alerts the user that wpcom account information is being retrieved.") - } -} - -// MARK: - View FLow - -extension LoginViewController { - func presentEpilogue(credentials: AuthenticatorCredentials) { - if mustShowSignupEpilogue() { - showSignupEpilogue(for: credentials) - } else if mustShowLoginEpilogue() { - showLoginEpilogue(for: credentials) - } else { - dismiss() - } - } -} - -// MARK: - Sync Helpers - -extension LoginViewController { - - /// Signals the Main App to synchronize the specified WordPress.com account. On completion, the epilogue will be pushed (if needed). - /// - func syncWPComAndPresentEpilogue( - credentials: AuthenticatorCredentials, - completion: (() -> Void)? = nil) { - - configureStatusLabel(LocalizedText.gettingAccountInfo) - - syncWPCom(credentials: credentials) { [weak self] in - guard let self else { - return - } - - completion?() - - self.presentEpilogue(credentials: credentials) - self.configureStatusLabel("") - self.configureViewLoading(false) - self.trackSignIn(credentials: credentials) - } - } - - /// Signals the Main App to synchronize the specified WordPress.com account. - /// - func syncWPCom(credentials: AuthenticatorCredentials, completion: (() -> Void)? = nil) { - authenticationDelegate.sync(credentials: credentials) { - completion?() - } - } - - /// Tracks the SignIn Event - /// - func trackSignIn(credentials: AuthenticatorCredentials) { - var properties = [String: String]() - - if let wpcom = credentials.wpcom { - properties = [ - "multifactor": wpcom.multifactor.description, - "dotcom_user": true.description - ] - } - - // This stat is part of a funnel that provides critical information. Please - // consult with your lead before removing this event. - WordPressAuthenticator.track(.signedIn, properties: properties) - tracker.track(step: .success) - } - - /// Links the current WordPress Account to a Social Service (if possible!!). - /// - func linkSocialServiceIfNeeded(loginFields: LoginFields, wpcomAuthToken: String) { - guard let serviceName = loginFields.meta.socialService, let serviceToken = loginFields.meta.socialServiceIDToken else { - return - } - - linkSocialService(serviceName: serviceName, - serviceToken: serviceToken, - wpcomAuthToken: wpcomAuthToken, - appleConnectParameters: loginFields.parametersForSignInWithApple) - } - - /// Links the current WordPress Account to a Social Service. - /// - func linkSocialService(serviceName: SocialServiceName, - serviceToken: String, - wpcomAuthToken: String, - appleConnectParameters: [String: AnyObject]? = nil) { - let service = WordPressComAccountService() - service.connect(wpcomAuthToken: wpcomAuthToken, - serviceName: serviceName, - serviceToken: serviceToken, - connectParameters: appleConnectParameters, - success: { - // This stat is part of a funnel that provides critical information. Please - // consult with your lead before removing this event. - let source = appleConnectParameters != nil ? "apple" : "google" - WordPressAuthenticator.track(.signedIn, properties: ["source": source]) - - if AuthenticatorAnalyticsTracker.shared.shouldUseLegacyTracker() { - WordPressAuthenticator.track(.loginSocialConnectSuccess) - WordPressAuthenticator.track(.loginSocialSuccess) - } - }, failure: { error in - WPLogError("Social Link Error: \(error)") - WordPressAuthenticator.track(.loginSocialConnectFailure, error: error) - // We're opting to let this call fail silently. - // Our user has already successfully authenticated and can use the app -- - // connecting the social service isn't critical. There's little to - // be gained by displaying an error that can not currently be resolved - // in the app and doing so might tarnish an otherwise satisfying login - // experience. - // If/when we add support for manually connecting/disconnecting services - // we can revisit. - }) - } -} - -// MARK: - Handle View Changes -// -extension LoginViewController { - - open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - // Update Dynamic Type - if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { - didChangePreferredContentSize() - } - - // Update Table View size - setTableViewMargins(forWidth: view.frame.width) - } - - open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - setTableViewMargins(forWidth: size.width) - } - - /// Resize the table view based on trait collection. - /// Used only in unified views. - /// - func setTableViewMargins(forWidth viewWidth: CGFloat) { - guard let tableViewLeadingConstraint, - let tableViewTrailingConstraint else { - return - } - - guard traitCollection.horizontalSizeClass == .regular && - traitCollection.verticalSizeClass == .regular else { - tableViewLeadingConstraint.constant = defaultTableViewMargin - tableViewTrailingConstraint.constant = defaultTableViewMargin - return - } - - let marginMultiplier = UIDevice.current.orientation.isLandscape ? - TableViewMarginMultipliers.ipadLandscape : - TableViewMarginMultipliers.ipadPortrait - - let margin = viewWidth * marginMultiplier - - tableViewLeadingConstraint.constant = margin - tableViewTrailingConstraint.constant = margin - } - - private enum TableViewMarginMultipliers { - static let ipadPortrait: CGFloat = 0.1667 - static let ipadLandscape: CGFloat = 0.25 - } -} - -// MARK: - Social Sign In Handling - -extension LoginViewController { - - func removeGoogleWaitingView() { - // Remove the Waiting for Google view so it doesn't reappear when backing through the navigation stack. - navigationController?.viewControllers.removeAll(where: { $0 is GoogleAuthViewController }) - } - - func signInAppleAccount() { - guard let token = loginFields.meta.socialServiceIDToken else { - WordPressAuthenticator.track(.loginSocialButtonFailure, properties: ["source": SocialServiceName.apple.rawValue]) - configureViewLoading(false) - return - } - - loginFacade.loginToWordPressDotCom(withSocialIDToken: token, service: SocialServiceName.apple.rawValue) - } - - // Used by SIWA when logging with with a passwordless, 2FA account. - // - func socialNeedsMultifactorCode(forUserID userID: Int, andNonceInfo nonceInfo: SocialLogin2FANonceInfo) { - loginFields.nonceInfo = nonceInfo - loginFields.nonceUserID = userID - - guard WordPressAuthenticator.shared.configuration.enableUnifiedAuth else { - presentLogin2FA() - return - } - - presentUnified2FA() - } - - private func presentLogin2FA() { - var properties = [AnyHashable: Any]() - if let service = loginFields.meta.socialService?.rawValue { - properties["source"] = service - } - - if tracker.shouldUseLegacyTracker() { - WordPressAuthenticator.track(.loginSocial2faNeeded, properties: properties) - } - - guard let vc = Login2FAViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginViewController to Login2FAViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - private func presentUnified2FA() { - - guard let vc = TwoFAViewController.instantiate(from: .twoFA) else { - WPLogError("Failed to navigate from LoginViewController to TwoFAViewController") - return - } - - vc.dismissBlock = dismissBlock - vc.loginFields = loginFields - navigationController?.pushViewController(vc, animated: true) - } -} - -// MARK: - LoginSocialError delegate methods - -extension LoginViewController: LoginSocialErrorViewControllerDelegate { - - func retryWithEmail() { - loginFields.username = "" - cleanupAfterSocialErrors() - navigationController?.popToRootViewController(animated: true) - } - - func retryWithAddress() { - cleanupAfterSocialErrors() - loginToSelfHostedSite() - } - - func retryAsSignup() { - cleanupAfterSocialErrors() - - if let controller = SignupEmailViewController.instantiate(from: .signup) { - controller.loginFields = loginFields - navigationController?.pushViewController(controller, animated: true) - } - } - - func errorDismissed() { - loginFields.username = "" - navigationController?.popToRootViewController(animated: true) - } - - private func cleanupAfterSocialErrors() { - dismiss(animated: true) {} - } - - /// Displays the self-hosted login form. - /// - @objc func loginToSelfHostedSite() { - guard WordPressAuthenticator.shared.configuration.enableUnifiedAuth else { - presentSelfHostedView() - return - } - - presentUnifiedSiteAddressView() - } - - /// Navigates to the unified site address login flow. - /// - func presentUnifiedSiteAddressView() { - guard let vc = SiteAddressViewController.instantiate(from: .siteAddress) else { - WPLogError("Failed to navigate from LoginViewController to SiteAddressViewController") - return - } - - navigationController?.pushViewController(vc, animated: true) - } - - /// Navigates to the old self-hosted login flow. - /// - func presentSelfHostedView() { - guard let vc = LoginSiteAddressViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginViewController to LoginSiteAddressViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginWPComViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginWPComViewController.swift deleted file mode 100644 index 78faf7464bc7..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginWPComViewController.swift +++ /dev/null @@ -1,258 +0,0 @@ -import UIKit -import WordPressShared -import WordPressKit - -/// Provides a form and functionality for signing a user in to WordPress.com -/// -class LoginWPComViewController: LoginViewController, NUXKeyboardResponder { - @IBOutlet weak var passwordField: WPWalkthroughTextField? - @IBOutlet weak var forgotPasswordButton: UIButton? - @IBOutlet weak var bottomContentConstraint: NSLayoutConstraint? - @IBOutlet weak var verticalCenterConstraint: NSLayoutConstraint? - @IBOutlet var emailIcon: UIImageView? - @IBOutlet var emailLabel: UITextField? - @IBOutlet var emailStackView: UIStackView? - override var sourceTag: WordPressSupportSourceTag { - get { - return .loginWPComPassword - } - } - - override var loginFields: LoginFields { - didSet { - // Clear the password (if any) from LoginFields. - loginFields.password = "" - } - } - - // MARK: - Lifecycle Methods - - override func viewDidLoad() { - super.viewDidLoad() - - localizeControls() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - // Update special case login fields. - loginFields.meta.userIsDotCom = true - - configureTextFields() - configureEmailIcon() - configureForgotPasswordButton() - configureSubmitButton(animating: false) - configureViewForEditingIfNeeded() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - - passwordField?.becomeFirstResponder() - WordPressAuthenticator.track(.loginPasswordFormViewed) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - - if isMovingFromParent { - // There was a bug that was causing iOS's update password prompt to come up - // when this VC was being dismissed pressing the "< Back" button. The following - // line ensures that such prompt doesn't come up anymore. - // - // More information can be found in the PR where this workaround is introduced: - // https://git.io/JUkak - // - passwordField?.text = "" - } - } - - // MARK: Setup and Configuration - - /// Configures the appearance and state of the submit button. - /// - override func configureSubmitButton(animating: Bool) { - submitButton?.showActivityIndicator(animating) - submitButton?.isEnabled = enableSubmit(animating: animating) - } - - override func enableSubmit(animating: Bool) -> Bool { - return !animating && - !loginFields.username.isEmpty && - !loginFields.password.isEmpty - } - - /// Configure the view's loading state. - /// - /// - Parameter loading: True if the form should be configured to a "loading" state. - /// - override func configureViewLoading(_ loading: Bool) { - passwordField?.isEnabled = !loading - - configureSubmitButton(animating: loading) - navigationItem.hidesBackButton = loading - } - - /// Configure the view for an editing state. Should only be called from viewWillAppear - /// as this method skips animating any change in height. - /// - @objc func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editiing state should be assumed. - // Check the helper to determine whether an editiing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - passwordField?.becomeFirstResponder() - } - } - - @objc func configureTextFields() { - passwordField?.text = loginFields.password - passwordField?.contentInsets = WPStyleGuide.edgeInsetForLoginTextFields() - emailLabel?.text = loginFields.username - emailLabel?.textColor = WordPressAuthenticator.shared.style.subheadlineColor - } - - func configureEmailIcon() { - guard let image = emailIcon?.image else { - return - } - emailIcon?.image = image.imageWithTintColor(WordPressAuthenticator.shared.style.subheadlineColor) - } - - private func configureForgotPasswordButton() { - guard let forgotPasswordButton else { - return - } - WPStyleGuide.configureTextButton(forgotPasswordButton) - } - - @objc func localizeControls() { - - instructionLabel?.text = { - guard let service = loginFields.meta.socialService else { - return NSLocalizedString("Enter the password for your WordPress.com account.", comment: "Instructional text shown when requesting the user's password for login.") - } - - if service == SocialServiceName.google { - return NSLocalizedString("To proceed with this Google account, please first log in with your WordPress.com password. This will only be asked once.", comment: "") - } - - return NSLocalizedString( - "Please enter the password for your WordPress.com account to log in with your Apple ID.", - comment: "Instructional text shown when requesting the user's password for a login initiated via Sign In with Apple" - ) - }() - - passwordField?.placeholder = NSLocalizedString("Password", comment: "Password placeholder") - passwordField?.accessibilityIdentifier = "Password" - - let submitButtonTitle = NSLocalizedString("Next", comment: "Title of a button. The text should be capitalized.").localizedCapitalized - submitButton?.setTitle(submitButtonTitle, for: .normal) - submitButton?.setTitle(submitButtonTitle, for: .highlighted) - submitButton?.accessibilityIdentifier = "Password Next Button" - - let forgotPasswordTitle = NSLocalizedString("Lost your password?", comment: "Title of a button. ") - forgotPasswordButton?.setTitle(forgotPasswordTitle, for: .normal) - forgotPasswordButton?.setTitle(forgotPasswordTitle, for: .highlighted) - forgotPasswordButton?.titleLabel?.numberOfLines = 0 - } - - // MARK: - Instance Methods - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with the submit action. - /// - @objc func validateForm() { - validateFormAndLogin() - } - - // MARK: - Actions - - @IBAction func handleTextFieldDidChange(_ sender: UITextField) { - switch sender { - case passwordField: - loginFields.password = sender.nonNilTrimmedText() - case emailLabel: - // The email can only be changed via a password manager. - // In this case, don't update username for social accounts. - // This prevents inadvertent account linking. - // Ref: https://git.io/JJSUM - if loginFields.meta.socialService != nil { - emailLabel?.text = loginFields.username - } else { - loginFields.username = sender.nonNilTrimmedText() - } - default: - break - } - - configureSubmitButton(animating: false) - } - - @IBAction func handleSubmitButtonTapped(_ sender: UIButton) { - validateForm() - } - - @IBAction func handleForgotPasswordButtonTapped(_ sender: UIButton) { - WordPressAuthenticator.openForgotPasswordURL(loginFields) - WordPressAuthenticator.track(.loginForgotPasswordClicked) - } - - override func displayRemoteError(_ error: Error) { - configureViewLoading(false) - - if (error as? WordPressComOAuthError)?.authenticationFailureKind == .invalidRequest { - let message = NSLocalizedString("It seems like you've entered an incorrect password. Want to give it another try?", comment: "An error message shown when a wpcom user provides the wrong password.") - displayError(message: message) - } else { - super.displayRemoteError(error) - } - } - - // MARK: - Dynamic type - - override func didChangePreferredContentSize() { - super.didChangePreferredContentSize() - emailLabel?.font = WPStyleGuide.fontForTextStyle(.body) - } - - // MARK: - Keyboard Notifications - - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } - - // MARK: Keyboard Events - - @objc func signinFormVerticalOffset() -> CGFloat { - // the stackview-based layout shifts fine with this adjustment - return 0 - } -} - -extension LoginWPComViewController: UITextFieldDelegate { - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if enableSubmit(animating: false) { - validateForm() - } - return true - } -} - -extension LoginWPComViewController { - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { - didChangePreferredContentSize() - } - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/SigninEditingState.swift b/Sources/WordPressAuthenticator/Features/SignIn/SigninEditingState.swift deleted file mode 100644 index e2ecf4f4aa7a..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/SigninEditingState.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - -open class SigninEditingState { - public static var signinEditingStateActive = false - public static var signinLastKeyboardHeightDelta: CGFloat = 0 -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/UIImageView+Additions.swift b/Sources/WordPressAuthenticator/Features/SignIn/UIImageView+Additions.swift deleted file mode 100644 index 02c99799b714..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/UIImageView+Additions.swift +++ /dev/null @@ -1,19 +0,0 @@ -import UIKit -import WordPressUI -import GravatarUI - -extension UIImageView { - func setGravatarImage(with email: String, placeholder: UIImage = .gravatarPlaceholderImage, rating: Rating = .general, preferredSize: CGSize? = nil, forceRefresh: Bool = false) async throws { - listenForGravatarChanges(forEmail: email) - var options: [ImageSettingOption] = [] - if forceRefresh { - options.append(.forceRefresh) - } - try await gravatar.setImage(avatarID: .email(email), - placeholder: placeholder, - rating: rating, - preferredSize: preferredSize, - defaultAvatarOption: .status404, - options: options) - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignUp/Signup.storyboard b/Sources/WordPressAuthenticator/Features/SignUp/Signup.storyboard deleted file mode 100644 index 94c12288ea9f..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignUp/Signup.storyboard +++ /dev/null @@ -1,186 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Features/SignUp/SignupEmailViewController.swift b/Sources/WordPressAuthenticator/Features/SignUp/SignupEmailViewController.swift deleted file mode 100644 index 0cd864b91f16..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignUp/SignupEmailViewController.swift +++ /dev/null @@ -1,239 +0,0 @@ -import UIKit -import WordPressShared -import WordPressKit - -class SignupEmailViewController: LoginViewController, NUXKeyboardResponder { - - // MARK: - NUXKeyboardResponder Properties - - @IBOutlet weak var bottomContentConstraint: NSLayoutConstraint? - @IBOutlet weak var verticalCenterConstraint: NSLayoutConstraint? - - // MARK: - Properties - - @IBOutlet weak var emailField: LoginTextField! - - override var sourceTag: WordPressSupportSourceTag { - get { - return .wpComSignupEmail - } - } - - private enum ErrorMessage: String { - case invalidEmail = "invalid_email" - case availabilityCheckFail = "availability_check_fail" - case emailUnavailable = "email_unavailable" - case magicLinkRequestFail = "magic_link_request_fail" - - func description() -> String { - switch self { - case .invalidEmail: - return NSLocalizedString("Please enter a valid email address.", comment: "Error message displayed when the user attempts use an invalid email address.") - case .availabilityCheckFail: - return NSLocalizedString("Unable to verify the email address. Please try again later.", comment: "Error message displayed when an error occurred checking for email availability.") - case .emailUnavailable: - return NSLocalizedString("Sorry, that email address is already being used!", comment: "Error message displayed when the entered email is not available.") - case .magicLinkRequestFail: - return NSLocalizedString("We were unable to send you an email at this time. Please try again later.", comment: "Error message displayed when an error occurred sending the magic link email.") - } - } - } - - // MARK: - View - - override func viewDidLoad() { - super.viewDidLoad() - localizeControls() - WordPressAuthenticator.track(.createAccountInitiated) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - configureViewForEditingIfNeeded() - - // If email address already exists, pre-populate it. - emailField.text = loginFields.emailAddress - - configureSubmitButton(animating: false) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - } - - private func localizeControls() { - instructionLabel?.text = NSLocalizedString("To create your new WordPress.com account, please enter your email address.", comment: "Text instructing the user to enter their email address.") - - emailField.placeholder = NSLocalizedString("Email address", comment: "Placeholder for a textfield. The user may enter their email address.") - emailField.accessibilityIdentifier = "Signup Email Address" - emailField.contentInsets = WPStyleGuide.edgeInsetForLoginTextFields() - - let submitButtonTitle = NSLocalizedString("Next", comment: "Title of a button. The text should be capitalized.").localizedCapitalized - submitButton?.setTitle(submitButtonTitle, for: .normal) - submitButton?.setTitle(submitButtonTitle, for: .highlighted) - submitButton?.accessibilityIdentifier = "Signup Email Next Button" - } - - /// Configure the view for an editing state. Should only be called from viewWillAppear - /// as this method skips animating any change in height. - /// - private func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - emailField.becomeFirstResponder() - } - } - - override func enableSubmit(animating: Bool) -> Bool { - return !animating && validEmail() - } - - // MARK: - Keyboard Notifications - - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } - - // MARK: - Email Validation - - private func validateForm() { - - // Hide the error label. - displayError(message: "") - - // If the email address is invalid, display appropriate message. - if !validEmail() { - displayError(message: ErrorMessage.invalidEmail.description()) - configureSubmitButton(animating: false) - return - } - - checkEmailAvailability { available in - if available { - self.loginFields.username = self.loginFields.emailAddress - self.loginFields.meta.emailMagicLinkSource = .signup - self.requestAuthenticationLink() - } - self.configureSubmitButton(animating: false) - } - } - - private func validEmail() -> Bool { - return EmailFormatValidator.validate(string: loginFields.emailAddress) - } - - // MARK: - Email Availability - - private func checkEmailAvailability(completion: @escaping (Bool) -> Void) { - - let remote = AccountServiceRemoteREST( - wordPressComRestApi: WordPressComRestApi(baseURL: WordPressAuthenticator.shared.configuration.wpcomAPIBaseURL)) - - remote.isEmailAvailable(loginFields.emailAddress, success: { [weak self] available in - if !available { - defer { - WordPressAuthenticator.track(.signupEmailToLogin) - } - // If the user has already signed up redirect to the Login flow - guard let vc = LoginEmailViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate to LoginEmailViewController from SignupEmailViewController") - return - } - - guard let self else { - return - } - - vc.loginFields.restrictToWPCom = true - vc.loginFields.username = self.loginFields.emailAddress - - self.navigationController?.pushViewController(vc, animated: true) - } - completion(available) - }, failure: { error in - guard let error else { - self.displayError(message: ErrorMessage.availabilityCheckFail.description()) - completion(false) - return - } - - WPLogError("Error checking email availability: \(error.localizedDescription)") - - switch error { - case AccountServiceRemoteError.emailAddressInvalid: - self.displayError(message: error.localizedDescription) - completion(false) - default: - self.displayError(message: ErrorMessage.availabilityCheckFail.description()) - completion(false) - } - }) - } - - // MARK: - Send email - - /// Makes the call to request a magic signup link be emailed to the user. - /// - private func requestAuthenticationLink() { - - configureSubmitButton(animating: true) - - let service = WordPressComAccountService() - service.requestSignupLink(for: loginFields.username, - success: { [weak self] in - self?.didRequestSignupLink() - self?.configureSubmitButton(animating: false) - }, failure: { [weak self] (_: Error) in - WPLogError("Request for signup link email failed.") - WordPressAuthenticator.track(.signupMagicLinkFailed) - self?.displayError(message: ErrorMessage.magicLinkRequestFail.description()) - self?.configureSubmitButton(animating: false) - }) - } - - private func didRequestSignupLink() { - WordPressAuthenticator.track(.signupMagicLinkRequested) - - guard let vc = NUXLinkMailViewController.instantiate(from: .emailMagicLink) else { - WPLogError("Failed to navigate to NUXLinkMailViewController") - return - } - - vc.loginFields = loginFields - vc.loginFields.restrictToWPCom = true - - navigationController?.pushViewController(vc, animated: true) - } - - // MARK: - Action Handling - - @IBAction func handleSubmit() { - displayError(message: "") - configureSubmitButton(animating: true) - validateForm() - } - - @IBAction func handleTextFieldDidChange(_ sender: UITextField) { - loginFields.emailAddress = emailField.nonNilTrimmedText() - configureSubmitButton(animating: false) - } - - // MARK: - Misc - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignUp/SignupGoogleViewController.swift b/Sources/WordPressAuthenticator/Features/SignUp/SignupGoogleViewController.swift deleted file mode 100644 index f6072704b7f0..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignUp/SignupGoogleViewController.swift +++ /dev/null @@ -1,80 +0,0 @@ -import UIKit -import WordPressShared - -/// View controller that handles the google signup flow -/// -class SignupGoogleViewController: LoginViewController { - - // MARK: - Properties - - private var hasShownGoogle = false - @IBOutlet var titleLabel: UILabel? - - override var sourceTag: WordPressSupportSourceTag { - get { - return .wpComSignupWaitingForGoogle - } - } - - // MARK: - View - - override func viewDidLoad() { - super.viewDidLoad() - titleLabel?.text = LocalizedText.waitingForGoogle - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - showGoogleScreenIfNeeded() - } -} - -// MARK: - Private Methods - -private extension SignupGoogleViewController { - - func showGoogleScreenIfNeeded() { - guard !hasShownGoogle else { - return - } - - // Flag this as a social sign in. - loginFields.meta.socialService = .google - - GoogleAuthenticator.sharedInstance.signupDelegate = self - GoogleAuthenticator.sharedInstance.showFrom(viewController: self, loginFields: loginFields, for: .signup) - - hasShownGoogle = true - } - - enum LocalizedText { - static let waitingForGoogle = NSLocalizedString("Waiting for Google to complete…", comment: "Message shown on screen while waiting for Google to finish its signup process.") - static let signupFailed = NSLocalizedString("Google sign up failed.", comment: "Message shown on screen after the Google sign up process failed.") - } -} - -// MARK: - GoogleAuthenticatorSignupDelegate - -extension SignupGoogleViewController: GoogleAuthenticatorSignupDelegate { - - func googleFinishedSignup(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - self.loginFields = loginFields - showSignupEpilogue(for: credentials) - } - - func googleLoggedInInstead(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - self.loginFields = loginFields - showLoginEpilogue(for: credentials) - } - - func googleSignupFailed(error: Error, loginFields: LoginFields) { - self.loginFields = loginFields - titleLabel?.textColor = .systemRed - titleLabel?.text = LocalizedText.signupFailed - displayError(error, sourceTag: .wpComSignup) - } - - func googleSignupCancelled() { - navigationController?.popViewController(animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignUp/SignupNavigationController.swift b/Sources/WordPressAuthenticator/Features/SignUp/SignupNavigationController.swift deleted file mode 100644 index 2bdab164f574..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignUp/SignupNavigationController.swift +++ /dev/null @@ -1,8 +0,0 @@ -import UIKit -import WordPressUI - -class SignupNavigationController: RotationAwareNavigationViewController { - override func viewDidLoad() { - super.viewDidLoad() - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Analytics/AuthenticatorAnalyticsTracker.swift b/Sources/WordPressAuthenticator/Helpers/Analytics/AuthenticatorAnalyticsTracker.swift deleted file mode 100644 index 438c873079d0..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Analytics/AuthenticatorAnalyticsTracker.swift +++ /dev/null @@ -1,513 +0,0 @@ -import Foundation -import WordPressShared - -/// Implements the analytics tracking logic for our sign in flow. -/// -public class AuthenticatorAnalyticsTracker { - - private static let defaultSource: Source = .default - private static let defaultFlow: Flow = .prologue - private static let defaultStep: Step = .prologue - - /// The method used for analytics tracking. Useful for overriding in automated tests. - /// - typealias TrackerMethod = (_ event: AnalyticsEvent) -> Void - - public enum EventType: String { - case step = "unified_login_step" - case interaction = "unified_login_interaction" - case failure = "unified_login_failure" - } - - public enum Property: String { - case failure - case flow - case click - case source - case step - } - - public enum Source: String { - /// Starts when the user logs in / sign up from the prologue screen - /// - case `default` - - case jetpack - case share - case deeplink - case reauthentication - - /// Starts when the used adds a site from the site picker - /// - case selfHosted = "self_hosted" - } - - public enum Flow: String { - /// The initial flow before we decide whether the user is logging in or signing up - /// - case wpCom = "wordpress_com" - - /// Flow for Google login - /// - case loginWithGoogle = "google_login" - - /// Flow for Google signup - /// - case signupWithGoogle = "google_signup" - - /// Flow for Apple login - /// - case loginWithApple = "siwa_login" - - /// Flow for Apple signup - /// - case signupWithApple = "siwa_signup" - - /// Flow for iCloud Keychain login - /// - case loginWithiCloudKeychain = "icloud_keychain_login" - - /// The flow that starts when we offer the user the magic link login - /// - case loginWithMagicLink = "login_magic_link" - - /// This flow starts when the user decides to login with a password instead - /// - case loginWithPassword = "login_password" - - /// This flow starts when the user decides to login with a password instead, with magic link logic emphasis - /// where the CTA is a secondary CTA instead of a table view row - /// - case loginWithPasswordWithMagicLinkEmphasis = "login_password_magic_link_emphasis" - - /// This flow starts when the user decides to log in with their site address - /// - case loginWithSiteAddress = "login_site_address" - - /// This flow starts when the user wants to troubleshoot their site by inputting its address - /// - case siteDiscovery = "site_discovery" - - /// This flow represents the signup (when the user inputs an email that’s not registered with a .com account) - /// - case signup - - /// This flow represents the prologue screen. - /// - case prologue - } - - public enum Step: String { - /// Gets shown on the Prologue screen - /// - case prologue - - /// Triggered when a flow is started - /// - case start - - /// Triggered when a user requests a magic link and sees the screen with the “Open mail” button - /// - case magicLinkRequested = "magic_link_requested" - - /// This represents the user opening their mail. It’s not strictly speaking an in-app screen but for the user it is part of the flow. - case emailOpened = "email_opened" - - /// Represents the screen or step in which WPCOM account email is entered by the user - /// - case enterEmailAddress = "enter_email_address" - - /// The screen with a username and password visible - /// - case usernamePassword = "username_password" - - /// The screen that requests the password - /// - case passwordChallenge = "password_challenge" - - /// Triggered on the epilogue screen - /// - case success - - /// Triggered on the help screen - /// - case help - - /// When we ask user to input the code from the 2 factor authentication - case twoFactorAuthentication = "2fa" - - /// Triggered when a user enters site credentials and sees the screen with instructions to verify email. (`VerifyEmailViewController`) - /// - case verifyEmailInstructions = "instructions_to_verify_email" - - /// Triggered when a magic link is automatically requested after filling in email address and the requested screen is shown - /// - case magicLinkAutoRequested = "magic_link_auto_requested" - } - - public enum ClickTarget: String { - /// Tracked when submitting the email form, the email & password form, site address form, - /// username & password form and signup email form - /// - case submit - - /// Tracked when the user clicks on continue in the login/signup epilogue - /// - case `continue` - - /// Tracked when the post signup interstitial screen is dismissed, when the - /// login signup help dialog is dismissed and when the email hint dialog is dismissed - /// - case dismiss - - /// Tracked when the user clicks “Continue with WordPress.com” on the Prologue screen - /// - case continueWithWordPressCom = "continue_with_wordpress_com" - - /// Tracked when the user clicks “What is WordPress.com?" button on the WordPress.com flow screen - /// - case whatIsWPCom = "what_is_wordpress_com" - - /// Tracked when the user clicks “Login with site address” on the Prologue screen - /// - case loginWithSiteAddress = "login_with_site_address" - - /// When the user tries to login with Apple from the confirmation screen - /// - case loginWithApple = "login_with_apple" - - /// Tracked when the user clicks “Login with Google” on the WordPress.com flow screen - /// - case loginWithGoogle = "login_with_google" - - /// When the user clicks on “Forgotten password” on one of the screens that show the password field - /// - case forgottenPassword = "forgotten_password" - - /// When the user clicks on terms of service anywhere - /// - case termsOfService = "terms_of_service_clicked" - - /// When the user tries to sign up with email from the confirmation screen - /// - case signupWithEmail = "signup_with_email" - - /// When the user tries to sign up with Apple from the confirmation screen - /// - case signupWithApple = "signup_with_apple" - - /// When the user tries to sign up with Google from the confirmation screen - /// - case signupWithGoogle = "signup_with_google" - - /// When the user opens the email client from the magic link screen - /// - case openEmailClient = "open_email_client" - - /// Any time the user clicks on the help icon in the login flow - /// - case showHelp = "show_help" - - /// Used on the 2FA screen to send code with a text instead of using the authenticator app - /// - case sendCodeWithText = "send_code_with_text" - - /// Used on the 2FA screen to use a security key instead of using the authenticator app - /// - case enterSecurityKey = "enter_security_key" - - /// Used on the 2FA screen to submit authentication code - /// - case submitTwoFactorCode = "submit_2fa_code" - - /// When the user requests a magic link after filling in email address - /// - case requestMagicLink = "request_magic_link" - - /// Click on “Create new site” button after a successful signup - /// - case createNewSite = "create_new_site" - - /// Adding a self-hosted site from the epilogue - /// - case addSelfHostedSite = "add_self_hosted_site" - - /// Connecting a site from the epilogue - /// - case connectSite = "connect_site" - - /// Picking an avatar from the epilogue after a successful signup - /// - case selectAvatar = "select_avatar" - - /// Editing the username from the epilogue after a successful signup - /// - case editUsername = "edit_username" - - /// Clicking on “Need help finding site address” from a dialog - /// - case helpFindingSiteAddress = "help_finding_site_address" - - /// When the user clicks on the email field to log in, this triggers the hint dialog to show up - /// - case selectEmailField = "select_email_field" - - /// When the user selects an email from the hint dialog - /// - case pickEmailFromHint = "pick_email_from_hint" - - /// When the user clicks on “Create account” on the signup confirmation screen - /// - case createAccount = "create_account" - - /// When the user taps of "Sign in with site credentials" button in `GetStartedViewController` - /// - case signInWithSiteCredentials = "sign_in_with_site_credentials" - - /// When the user clicks on “Login with account password” on `VerifyEmailViewController` - /// - case loginWithAccountPassword = "login_with_password" - } - - public enum Failure: String { - /// Failure to guess XMLRPC URL - /// - case loginFailedToGuessXMLRPC = "login_failed_to_guess_xmlrpc_url" - } - - /// Shared Instance. - /// - public static var shared: AuthenticatorAnalyticsTracker = { - return AuthenticatorAnalyticsTracker() - }() - - /// State for the analytics tracker. - /// - public class State { - internal(set) public var lastFlow: Flow - internal(set) public var lastSource: Source - internal(set) public var lastStep: Step - - init(lastFlow: Flow = AuthenticatorAnalyticsTracker.defaultFlow, lastSource: Source = AuthenticatorAnalyticsTracker.defaultSource, lastStep: Step = AuthenticatorAnalyticsTracker.defaultStep) { - self.lastFlow = lastFlow - self.lastSource = lastSource - self.lastStep = lastStep - } - } - - /// The state of this tracker. - /// - public let state = State() - - /// The backing analytics tracking method. Can be overridden for testing purposes. - /// - let track: TrackerMethod - - /// Whether tracking is enabled or not. This is just a convenience configuration to enable this tracker to be turned on and off - /// using a feature flag. It should go away once we remove the legacy tracking. - /// - let enabled: Bool - - // MARK: - Initializers - - init(enabled: Bool = WordPressAuthenticator.shared.configuration.enableUnifiedAuth, track: @escaping TrackerMethod = WPAnalytics.track) { - self.enabled = enabled - self.track = track - } - - /// Resets the flow and step to the defaults. The source is left untouched, and should only be set explicitly. - /// - func resetState() { - set(flow: Self.defaultFlow) - set(step: Self.defaultStep) - } - - // MARK: - Legacy vs Unified tracking - - /// This method will reply whether, for the current flow in the state, tracking is enabled. - /// - /// It's the responsibility of the class calling the tracking methods to check this before attempting to actually do the tracking. - /// - /// - Returns: `true` if we can track using the state machine. - /// - public func canTrack() -> Bool { - return enabled - } - - /// This is a convenience method, that's useful for cases where we simply want to check if the legacy tracking should be - /// enabled. It can be particularly useful in cases where we don't have a matching tracking call in the new flow. - /// - /// - Returns: `true` if we must use legacy tracking, `false` otherwise. - /// - public func shouldUseLegacyTracker() -> Bool { - return !canTrack() - } - - // MARK: - Tracking - - /// Track a step within a flow. - /// - public func track(step: Step) { - guard canTrack() else { - return - } - - track(event(step: step)) - } - - /// Track a click interaction. - /// - public func track(click: ClickTarget) { - guard canTrack() else { - return - } - - track(event(click: click)) - } - - /// Track a predefined failure enum. - /// - public func track(failure: Failure) { - track(failure: failure.rawValue) - } - - /// Track a failure. - /// - public func track(failure: String) { - guard canTrack() else { - return - } - - track(event(failure: failure)) - } - - // MARK: - Tracking: Legacy Tracking Support - - /// Tracks a step within a flow if tracking is enabled for that flow, or executes the specified block if tracking is not enabled - /// for the flow. - /// - public func track(step: Step, ifTrackingNotEnabled legacyTracking: () -> Void) { - guard canTrack() else { - legacyTracking() - return - } - - track(step: step) - } - - /// Track a click interaction if tracking is enabled for that flow, or executes the specified block if tracking is not enabled - /// for the flow. - /// - public func track(click: ClickTarget, ifTrackingNotEnabled legacyTracking: () -> Void) { - guard canTrack() else { - legacyTracking() - return - } - - track(event(click: click)) - } - - /// Track a failure if tracking is enabled for that flow, or executes the specified block if tracking is not enabled - /// for the flow. - /// - public func track(failure: String, ifTrackingNotEnabled legacyTracking: () -> Void) { - guard canTrack() else { - legacyTracking() - return - } - - track(event(failure: failure)) - } - - // MARK: - Event Construction & Context Updating - - /// Creates an event for a step. Updates the state machine. - /// - /// - Parameters: - /// - step: the step we're tracking. - /// - flow: the flow that the step belongs to. - /// - /// - Returns: an analytics event representing the step. - /// - private func event(step: Step) -> AnalyticsEvent { - let event = AnalyticsEvent( - name: EventType.step.rawValue, - properties: properties(step: step)) - - state.lastStep = step - - return event - } - - /// Creates an event for a failure. Loads the properties from the state machine. - /// - /// - Parameters: - /// - failure: the error message we want to track. - /// - /// - Returns: an analytics event representing the failure. - /// - private func event(failure: String) -> AnalyticsEvent { - var properties = lastProperties() - properties[Property.failure.rawValue] = failure - - return AnalyticsEvent( - name: EventType.failure.rawValue, - properties: properties) - } - - /// Creates an event for a click interaction. Loads the properties from the state machine. - /// - /// - Parameters: - /// - click: the target of the click interaction. - /// - /// - Returns: an analytics event representing the click interaction. - /// - private func event(click: ClickTarget) -> AnalyticsEvent { - var properties = lastProperties() - properties[Property.click.rawValue] = click.rawValue - - return AnalyticsEvent( - name: EventType.interaction.rawValue, - properties: properties) - } - - // MARK: - Source & Flow - - /// Allows the caller to set the flow without tracking. - /// - public func set(flow: Flow) { - state.lastFlow = flow - } - - /// Allows the caller to set the source without tracking. - /// - public func set(source: Source) { - state.lastSource = source - } - - /// Allows the caller to set the step without tracking. - /// - public func set(step: Step) { - state.lastStep = step - } - - // MARK: - Properties - - private func properties(step: Step) -> [String: String] { - return properties(step: step, flow: state.lastFlow, source: state.lastSource) - } - - private func properties(step: Step, flow: Flow, source: Source) -> [String: String] { - return [ - Property.flow.rawValue: flow.rawValue, - Property.source.rawValue: source.rawValue, - Property.step.rawValue: step.rawValue - ] - } - - /// Retrieve the last step, flow and source stored in the state machine. - /// - private func lastProperties() -> [String: String] { - return properties(step: state.lastStep, flow: state.lastFlow, source: state.lastSource) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Errors.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Errors.swift deleted file mode 100644 index 7d86cb7b6b28..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Errors.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -// MARK: - WordPressAuthenticator Error Constants. Once the entire code is Swifted, let's *PLEASE* have a -// beautiful Error `Swift Enum`. -// -extension WordPressAuthenticator { - - /// Error Domain for Authentication issues. - /// - @objc public static let errorDomain = "org.wordpress.ios.authenticator" - - /// "Invalid Version" Error Code. Used whenever the remote WordPress.org endpoint is below the supported version. - /// - @objc public static let invalidVersionErrorCode = 5000 -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Events.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Events.swift deleted file mode 100644 index 774056aaaf77..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Events.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation -import WordPressShared - -// MARK: - Authentication Flow Event. Useful to relay internal Auth events over to activity trackers. -// -extension WordPressAuthenticator { - - /// Tracks the specified event. - /// - @objc - public static func track(_ event: WPAnalyticsStat) { - WordPressAuthenticator.shared.delegate?.track(event: event) - } - - /// Tracks the specified event, with the specified properties. - /// - @objc - public static func track(_ event: WPAnalyticsStat, properties: [AnyHashable: Any]) { - WordPressAuthenticator.shared.delegate?.track(event: event, properties: properties) - } - - /// Tracks the specified event, with the associated Error. - /// - /// Note: Ideally speaking... `Error` is not optional. *However* this method is to be used in the ObjC realm, where not everything - /// has it's nullability specifier set. We're just covering unexpected scenarios. - /// - @objc - public static func track(_ event: WPAnalyticsStat, error: Error?) { - guard let error else { - track(event) - return - } - - WordPressAuthenticator.shared.delegate?.track(event: event, error: error) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Notifications.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Notifications.swift deleted file mode 100644 index 7c2c25cc8718..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Notifications.swift +++ /dev/null @@ -1,11 +0,0 @@ -// MARK: - WordPressAuthenticator-Y Notifications -// -extension NSNotification.Name { - /// Posted whenever the Login Flow has been cancelled. - /// - public static let wordpressLoginCancelled = Foundation.Notification.Name(rawValue: "WordPressLoginCancelled") - - /// Posted whenever a Jetpack Login was successfully performed. - /// - public static let wordpressLoginFinishedJetpackLogin = Foundation.Notification.Name(rawValue: "WordPressLoginFinishedJetpackLogin") -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator.swift deleted file mode 100644 index be3086998416..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator.swift +++ /dev/null @@ -1,547 +0,0 @@ -import AuthenticationServices -import NSURL_IDN -import UIKit -import WordPressShared -import WordPressKit - -// MARK: - WordPressAuthenticator: Public API to deal with WordPress.com and WordPress.org authentication. -// -@objc public class WordPressAuthenticator: NSObject { - - /// (Private) Shared Instance. - /// - private static var privateInstance: WordPressAuthenticator? - - /// Observer for AppleID Credential State - /// - private var appleIDCredentialObserver: NSObjectProtocol? - - /// Optional sign in source that could be from the login prologue or the host app to track the entry point - /// for customizations in the epilogue handling. - var signInSource: SignInSource? - - /// Shared Instance. - /// - @objc public static var shared: WordPressAuthenticator { - guard let privateInstance else { - fatalError("WordPressAuthenticator wasn't initialized") - } - - return privateInstance - } - - /// Authenticator's Delegate. - /// - public weak var delegate: WordPressAuthenticatorDelegate? - - /// Authenticator's Configuration. - /// - public let configuration: WordPressAuthenticatorConfiguration - - /// Authenticator's Styles. - /// - public let style: WordPressAuthenticatorStyle - - /// Authenticator's Styles for unified flows. - /// - public let unifiedStyle: WordPressAuthenticatorUnifiedStyle? - - /// Authenticator's Display Images. - /// - public let displayImages: WordPressAuthenticatorDisplayImages - - /// Authenticator's Display Texts. - /// - public let displayStrings: WordPressAuthenticatorDisplayStrings - - /// Notification to be posted whenever the signing flow completes. - /// - @objc public static let WPSigninDidFinishNotification = "WPSigninDidFinishNotification" - - /// The host name that identifies magic link URLs - /// - private static let magicLinkUrlHostname = "magic-login" - - // MARK: - Initialization - - /// Designated Initializer - /// - init(configuration: WordPressAuthenticatorConfiguration, - style: WordPressAuthenticatorStyle, - unifiedStyle: WordPressAuthenticatorUnifiedStyle?, - displayImages: WordPressAuthenticatorDisplayImages, - displayStrings: WordPressAuthenticatorDisplayStrings) { - self.configuration = configuration - self.style = style - self.unifiedStyle = unifiedStyle - self.displayImages = displayImages - self.displayStrings = displayStrings - } - - /// Initializes the WordPressAuthenticator with the specified Configuration. - /// - public static func initialize(configuration: WordPressAuthenticatorConfiguration, - style: WordPressAuthenticatorStyle, - unifiedStyle: WordPressAuthenticatorUnifiedStyle?, - displayImages: WordPressAuthenticatorDisplayImages = .defaultImages, - displayStrings: WordPressAuthenticatorDisplayStrings = .defaultStrings) { - privateInstance = WordPressAuthenticator(configuration: configuration, - style: style, - unifiedStyle: unifiedStyle, - displayImages: displayImages, - displayStrings: displayStrings) - } - - // MARK: - Testing Support - - class func isInitialized() -> Bool { - return privateInstance != nil - } - - // MARK: - Public Methods - - /// Indicates if the specified ViewController belongs to the Authentication Flow, or not. - /// - public class func isAuthenticationViewController(_ viewController: UIViewController) -> Bool { - return viewController is NUXViewControllerBase - } - - /// Indicates if the received URL is a Google Authentication Callback. - /// - @objc public func isGoogleAuthUrl(_ url: URL) -> Bool { - return url.absoluteString.hasPrefix(configuration.googleLoginScheme) - } - - /// Indicates if the received URL is a WordPress.com Authentication Callback. - /// - @objc public func isWordPressAuthUrl(_ url: URL) -> Bool { - let expectedPrefix = configuration.wpcomScheme + "://" + Self.magicLinkUrlHostname - return url.absoluteString.hasPrefix(expectedPrefix) - } - - /// Attempts to process the specified URL as a WordPress Authentication Link. Returns *true* on success. - /// - @objc public func handleWordPressAuthUrl(_ url: URL, rootViewController: UIViewController, automatedTesting: Bool = false) -> Bool { - return WordPressAuthenticator.openAuthenticationURL(url, fromRootViewController: rootViewController, automatedTesting: automatedTesting) - } - - // MARK: - Helpers for presenting the login flow - - /// Used to present the new login flow from the app delegate - @objc public class func showLoginFromPresenter(_ presenter: UIViewController, animated: Bool) { - showLogin(from: presenter, animated: animated) - } - - /// Shows login UI from the given presenter view controller. - /// - /// - Parameters: - /// - presenter: The view controller that presents the login UI. - /// - animated: Whether the login UI is presented with animation. - /// - showCancel: Whether a cancel CTA is shown on the login prologue screen. - /// - restrictToWPCom: Whether only WordPress.com login is enabled. - /// - onLoginButtonTapped: Called when the login button on the prologue screen is tapped. - /// - onCompletion: Called when the login UI presentation completes. - public class func showLogin(from presenter: UIViewController, animated: Bool, showCancel: Bool = false, restrictToWPCom: Bool = false, onLoginButtonTapped: (() -> Void)? = nil, onCompletion: (() -> Void)? = nil) { - guard let loginViewController = loginUI(showCancel: showCancel, restrictToWPCom: restrictToWPCom, onLoginButtonTapped: onLoginButtonTapped) else { - return - } - presenter.present(loginViewController, animated: animated, completion: onCompletion) - trackOpenedLogin() - } - - /// Returns the view controller for the login flow. - /// The caller is responsible for tracking `.openedLogin` event when displaying the view controller as in `showLogin`. - /// - /// - Parameters: - /// - showCancel: Whether a cancel CTA is shown on the login prologue screen. - /// - restrictToWPCom: Whether only WordPress.com login is enabled. - /// - onLoginButtonTapped: Called when the login button on the prologue screen is tapped. - /// - Returns: The root view controller for the login flow. - public class func loginUI(showCancel: Bool = false, restrictToWPCom: Bool = false, onLoginButtonTapped: (() -> Void)? = nil, continueWithDotCom: ((UIViewController) -> Bool)? = nil, selfHostedSiteLogin: ((UIViewController) -> Bool)? = nil) -> UIViewController? { - let storyboard = Storyboard.login.instance - guard let controller = storyboard.instantiateInitialViewController() else { - assertionFailure("Cannot instantiate initial login controller from Login.storyboard") - return nil - } - - if let loginNavController = controller as? LoginNavigationController, let loginPrologueViewController = loginNavController.viewControllers.first as? LoginPrologueViewController { - loginPrologueViewController.showCancel = showCancel - loginPrologueViewController.continueWithDotComOverwrite = continueWithDotCom - loginPrologueViewController.selfHostedSiteLoginOverwrite = selfHostedSiteLogin - } - - controller.modalPresentationStyle = .fullScreen - return controller - } - - /// Used to present the new wpcom-only login flow from the app delegate - @objc public class func showLoginForJustWPCom(from presenter: UIViewController, jetpackLogin: Bool = false, connectedEmail: String? = nil, siteURL: String? = nil) { - defer { - trackOpenedLogin() - } - guard WordPressAuthenticator.shared.configuration.enableUnifiedAuth else { - showEmailLogin(from: presenter, jetpackLogin: jetpackLogin, connectedEmail: connectedEmail, siteURL: siteURL) - return - } - - showGetStarted(from: presenter, jetpackLogin: jetpackLogin, connectedEmail: connectedEmail, siteURL: siteURL) - } - - /// Used to present the Verify Email flow from the app delegate. - /// - /// - Parameters: - /// - presenter: The view controller that presents the Verify Email view. - /// - xmlrpc: The URL to reach the XMLRPC file of the site to log in to. - /// - connectedEmail: The email address used to authorized Jetpack connection with the site. - /// - siteURL: The URL of the site to log in to. - /// - @objc public class func showVerifyEmailForWPCom(from presenter: UIViewController, xmlrpc: String, connectedEmail: String, siteURL: String) { - let loginFields = LoginFields() - loginFields.meta.xmlrpcURL = NSURL(string: xmlrpc) - loginFields.username = connectedEmail - loginFields.siteAddress = siteURL - - guard let vc = VerifyEmailViewController.instantiate(from: .verifyEmail) else { - WPLogError("Failed to navigate to VerifyEmailViewController") - return - } - - vc.loginFields = loginFields - let navController = LoginNavigationController(rootViewController: vc) - navController.modalPresentationStyle = .fullScreen - presenter.present(navController, animated: true, completion: nil) - } - - /// Used to present the site credential login flow directly from the delegate. - /// - /// - Parameters: - /// - presenter: The view controller that presents the site credential login flow. - /// - siteURL: The URL of the site to log in to. - /// - onCompletion: The closure to be trigged when the login succeeds with the input credentials. - /// - public class func showSiteCredentialLogin(from presenter: UIViewController, siteURL: String, onCompletion: @escaping (WordPressOrgCredentials) -> Void) { - let controller = SiteCredentialsViewController.instantiate(from: .siteAddress) { coder in - SiteCredentialsViewController(coder: coder, isDismissible: true, onCompletion: onCompletion) - } - guard let controller else { - WPLogError("Failed to navigate from GetStartedViewController to SiteCredentialsViewController") - return - } - - let loginFields = LoginFields() - loginFields.siteAddress = siteURL - controller.loginFields = loginFields - controller.dismissBlock = { _ in - controller.navigationController?.dismiss(animated: true) - } - - let navController = LoginNavigationController(rootViewController: controller) - navController.modalPresentationStyle = .fullScreen - presenter.present(navController, animated: true, completion: nil) - } - - /// A helper method to fetch site info for a given URL. - /// - Parameters: - /// - siteURL: The URL of the site to fetch information for. - /// - onCompletion: The closure to be triggered when fetching site info is done. - /// - public class func fetchSiteInfo(for siteURL: String, onCompletion: @escaping (Result) -> Void) { - let service = WordPressComBlogService() - service.fetchUnauthenticatedSiteInfoForAddress(for: siteURL, success: { siteInfo in - onCompletion(.success(siteInfo)) - }, failure: { error in - onCompletion(.failure(error)) - }) - } - - /// Shows the unified Login/Signup flow. - /// - private class func showGetStarted(from presenter: UIViewController, jetpackLogin: Bool, connectedEmail: String? = nil, siteURL: String? = nil) { - guard let controller = GetStartedViewController.instantiate(from: .getStarted) else { - WPLogError("Failed to navigate from LoginPrologueViewController to GetStartedViewController") - return - } - - controller.loginFields.restrictToWPCom = true - controller.loginFields.username = connectedEmail ?? String() - controller.loginFields.meta.jetpackLogin = jetpackLogin - if let siteURL { - controller.loginFields.siteAddress = siteURL - } - - let navController = LoginNavigationController(rootViewController: controller) - navController.modalPresentationStyle = .fullScreen - presenter.present(navController, animated: true, completion: nil) - } - - /// Shows the Email Login view with Signup option. - /// - private class func showEmailLogin(from presenter: UIViewController, jetpackLogin: Bool, connectedEmail: String? = nil, siteURL: String? = nil) { - guard let controller = LoginEmailViewController.instantiate(from: .login) else { - return - } - - controller.loginFields.restrictToWPCom = true - controller.loginFields.meta.jetpackLogin = jetpackLogin - if let siteURL { - controller.loginFields.siteAddress = siteURL - } - - if let email = connectedEmail { - controller.loginFields.username = email - } else { - controller.offerSignupOption = true - } - - let navController = LoginNavigationController(rootViewController: controller) - navController.modalPresentationStyle = .fullScreen - presenter.present(navController, animated: true, completion: nil) - } - - @objc public class func showLoginForSelfHostedSite(_ presenter: UIViewController) { - defer { - trackOpenedLogin() - } - - AuthenticatorAnalyticsTracker.shared.set(source: .selfHosted) - - guard let controller = signinForWPOrg() else { - WPLogError("WordPressAuthenticator: Failed to instantiate Site Address view controller.") - return - } - - let navController = LoginNavigationController(rootViewController: controller) - navController.modalPresentationStyle = .fullScreen - presenter.present(navController, animated: true, completion: nil) - } - - /// Returns a Site Address view controller: allows the user to log into a WordPress.org website. - /// - @objc public class func signinForWPOrg() -> UIViewController? { - guard WordPressAuthenticator.shared.configuration.enableUnifiedAuth else { - return LoginSiteAddressViewController.instantiate(from: .login) - } - - return SiteAddressViewController.instantiate(from: .siteAddress) - } - - /// Returns a Site Address view controller and triggers the protocol method `troubleshootSite` after fetching the site info. - /// - @objc public class func siteDiscoveryUI() -> UIViewController? { - return SiteAddressViewController.instantiate(from: .siteAddress) { coder in - SiteAddressViewController(isSiteDiscovery: true, coder: coder) - } - } - - @objc - public class func signinForWPCom(dotcomEmailAddress: String?, dotcomUsername: String?, onDismissed: ((_ cancelled: Bool) -> Void)? = nil) -> UIViewController { - let loginFields = LoginFields() - loginFields.emailAddress = dotcomEmailAddress ?? String() - loginFields.username = dotcomUsername ?? String() - - guard WordPressAuthenticator.shared.configuration.enableUnifiedAuth else { - guard let controller = LoginWPComViewController.instantiate(from: .login) else { - WPLogError("WordPressAuthenticator: Failed to instantiate LoginWPComViewController") - return UIViewController() - } - - controller.loginFields = loginFields - controller.dismissBlock = onDismissed - - return NUXNavigationController(rootViewController: controller) - } - - AuthenticatorAnalyticsTracker.shared.set(source: .reauthentication) - AuthenticatorAnalyticsTracker.shared.set(flow: .loginWithPassword) - - guard let controller = PasswordViewController.instantiate(from: .password) else { - WPLogError("WordPressAuthenticator: Failed to instantiate PasswordViewController") - return UIViewController() - } - - controller.loginFields = loginFields - controller.dismissBlock = onDismissed - controller.trackAsPasswordChallenge = false - - return NUXNavigationController(rootViewController: controller) - } - - /// Returns an instance of LoginEmailViewController. - /// This allows the host app to configure the controller's features. - /// - public class func signinForWPCom() -> LoginEmailViewController { - guard let controller = LoginEmailViewController.instantiate(from: .login) else { - fatalError() - } - - return controller - } - - private class func trackOpenedLogin() { - WordPressAuthenticator.track(.openedLogin) - } - - // MARK: - Authentication Link Helpers - - /// Present a signin view controller to handle an authentication link. - /// - /// - Parameters: - /// - url: The authentication URL - /// - rootViewController: The view controller to act as the presenter for the signin view controller. By convention this is the app's root vc. - /// - automatedTesting: for calling this method for automated testing. It won't sync the account or load any other VCs. - /// - @objc public class func openAuthenticationURL( - _ url: URL, - fromRootViewController rootViewController: UIViewController, - automatedTesting: Bool = false) -> Bool { - - guard let queryDictionary = url.query?.dictionaryFromQueryString() else { - WPLogError("Magic link error: we couldn't retrieve the query dictionary from the sign-in URL.") - return false - } - - guard let authToken = queryDictionary["token"] as? String else { - WPLogError("Magic link error: we couldn't retrieve the authentication token from the sign-in URL.") - return false - } - - guard let flowRawValue = queryDictionary["flow"] as? String else { - WPLogError("Magic link error: we couldn't retrieve the flow from the sign-in URL.") - return false - } - - let loginFields = LoginFields() - - if url.isJetpackConnect { - loginFields.meta.jetpackLogin = true - } - - // We could just use the flow, but since `MagicLinkFlow` is an ObjC enum, it always - // allows a `default` value. By mapping the ObjC enum to a Swift enum we can avoid that afterwards. - let flow: NUXLinkAuthViewController.Flow - - switch MagicLinkFlow(rawValue: flowRawValue) { - case .signup: - flow = .signup - loginFields.meta.emailMagicLinkSource = .signup - Self.track(.signupMagicLinkOpened) - case .login: - flow = .login - loginFields.meta.emailMagicLinkSource = .login - Self.track(.loginMagicLinkOpened) - default: - WPLogError("Magic link error: the flow should be either `signup` or `login`. We can't handle an unsupported flow.") - return false - } - - if !automatedTesting { - let storyboard = Storyboard.emailMagicLink.instance - guard let loginVC = storyboard.instantiateViewController(withIdentifier: "LinkAuthView") as? NUXLinkAuthViewController else { - WPLogInfo("App opened with authentication link but couldn't create login screen.") - return false - } - loginVC.loginFields = loginFields - - let navController = LoginNavigationController(rootViewController: loginVC) - navController.modalPresentationStyle = .fullScreen - - // The way the magic link flow works some view controller might - // still be presented when the app is resumed by tapping on the auth link. - // We need to do a little work to present the SigninLinkAuth controller - // from the right place. - // - If the rootViewController is not presenting another vc then just - // present the auth controller. - // - If the rootViewController is presenting another NUX vc, dismiss the - // NUX vc then present the auth controller. - // - If the rootViewController is presenting *any* other vc, present the - // auth controller from the presented vc. - let presenter = rootViewController.topmostPresentedViewController - if presenter.isKind(of: NUXNavigationController.self) || presenter.isKind(of: LoginNavigationController.self), - let parent = presenter.presentingViewController { - parent.dismiss(animated: false, completion: { - parent.present(navController, animated: false, completion: nil) - }) - } else { - presenter.present(navController, animated: false, completion: nil) - } - - loginVC.syncAndContinue(authToken: authToken, flow: flow, isJetpackConnect: url.isJetpackConnect) - } - - return true - } - - // MARK: - Site URL helper - - /// The base site URL path derived from `loginFields.siteUrl` - /// - /// - Parameter string: The source URL as a string. - /// - /// - Returns: The base URL or an empty string. - /// - public class func baseSiteURL(string: String) -> String { - - guard !string.isEmpty, - let siteURL = NSURL(string: NSURL.idnEncodedURL(string)), - var path = siteURL.absoluteString else { - return "" - } - - let isSiteURLSchemeEmpty = siteURL.scheme == nil || siteURL.scheme!.isEmpty - - if isSiteURLSchemeEmpty { - path = "https://\(path)" - } else if path.isWordPressComPath() && path.contains("http://") { - path = path.replacingOccurrences(of: "http://", with: "https://") - } - - path.removeSuffix("/wp-login.php") - - // Remove wp-admin and everything after it. - try? path.removeSuffix(pattern: "/wp-admin(.*)") - - path.removeSuffix("/") - - return path - } - - // MARK: - Other Helpers - - /// Opens Safari to display the forgot password page for a wpcom or self-hosted - /// based on the passed LoginFields instance. - /// - /// - Parameter loginFields: A LoginFields instance. - /// - public class func openForgotPasswordURL(_ loginFields: LoginFields) { - let baseURL = loginFields.meta.userIsDotCom ? "https://wordpress.com" : WordPressAuthenticator.baseSiteURL(string: loginFields.siteAddress) - let forgotPasswordURL = URL(string: baseURL + "/wp-login.php?action=lostpassword&redirect_to=wordpress%3A%2F%2F")! - UIApplication.shared.open(forgotPasswordURL) - } - - public class var bundle: Bundle { - Bundle(for: WordPressAuthenticator.self) - } -} - -public extension WordPressAuthenticator { - - func getAppleIDCredentialState(for userID: String, completion: @escaping (ASAuthorizationAppleIDProvider.CredentialState, Error?) -> Void) { - AppleAuthenticator.sharedInstance.getAppleIDCredentialState(for: userID) { state, error in - // If credentialState == .notFound, error will have a value. - completion(state, error) - } - } - - func startObservingAppleIDCredentialRevoked(completion: @escaping () -> Void) { - appleIDCredentialObserver = NotificationCenter.default.addObserver(forName: AppleAuthenticator.credentialRevokedNotification, object: nil, queue: nil) { _ in - completion() - } - } - - func stopObservingAppleIDCredentialRevoked() { - if let observer = appleIDCredentialObserver { - NotificationCenter.default.removeObserver(observer) - } - appleIDCredentialObserver = nil - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorConfiguration.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorConfiguration.swift deleted file mode 100644 index 681f0d5fab3e..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorConfiguration.swift +++ /dev/null @@ -1,262 +0,0 @@ -import WordPressKit - -// MARK: - WordPressAuthenticator Configuration -// -public struct WordPressAuthenticatorConfiguration { - - /// WordPress.com Client ID - /// - let wpcomClientId: String - - /// WordPress.com Secret - /// - let wpcomSecret: String - - /// Client App: Used for Magic Link purposes. - /// - let wpcomScheme: String - - /// WordPress.com Terms of Service URL - /// - let wpcomTermsOfServiceURL: URL - - /// WordPress.com Base URL for OAuth - /// - let wpcomBaseURL: URL - - /// WordPress.com API Base URL - /// - let wpcomAPIBaseURL: URL - - /// The URL of a webpage which has details about What is WordPress.com?. - /// - /// Displayed in the WordPress.com login page. The button/link will not be displayed if this value is nil. - /// - let whatIsWPComURL: URL? - - /// GoogleLogin Client ID - /// - let googleLoginClientId: String - - /// GoogleLogin ServerClient ID - /// - let googleLoginServerClientId: String - - /// GoogleLogin Callback Scheme - /// - let googleLoginScheme: String - - internal var googleClientId: GoogleClientId { - guard let clientId = GoogleClientId(string: googleLoginClientId) else { - fatalError("Could not init GoogleClientId from developer provided value.") - } - - return clientId - } - - /// UserAgent - /// - let userAgent: String - - /// Flag indicating which Log In flow to display. - /// If enabled, when Log In is selected, a button view is displayed with options. - /// If disabled, when Log In is selected, the email login view is displayed with alternative options. - /// - let showLoginOptions: Bool - - /// Flag indicating if Sign Up UX is enabled for all services. - /// - let enableSignUp: Bool - - /// Hint buttons help users complete a step in the unified auth flow. Enabled by default. - /// If enabled, "Find your site address", "Reset your password", and others will be displayed. - /// If disabled, none of the hint buttons will appear on the unified auth flows. - let displayHintButtons: Bool - - /// Flag indicating if the Sign In With Apple option should be displayed. - /// - let enableSignInWithApple: Bool - - /// Flag indicating if signing up via Google is enabled. - /// This only applies to the unified Google flow. - /// When a user attempts to log in with a nonexistent account: - /// If enabled, the user will be redirected to Google signup. - /// If disabled, a view is displayed providing the user with other options. - /// - let enableSignupWithGoogle: Bool - - /// Flag for the unified login/signup flows. - /// If disabled, none of the unified flows will display. - /// If enabled, all unified flows will display. - /// - let enableUnifiedAuth: Bool - - /// Flag for the new prologue carousel. - /// If disabled, displays the old carousel. - /// If enabled, displays the new carousel. - let enableUnifiedCarousel: Bool - - /// Flag for the Passkeys, or WebAuthn, login. - let enablePasskeys: Bool - - /// Flag for the unified login/signup flows. - /// If disabled, the "Continue With WordPress" button in the login prologue is shown first. - /// If enabled, the "Enter your existing site address" button in the login prologue is shown first. - /// Default value is disabled - let continueWithSiteAddressFirst: Bool - - /// If enabled shows a "Sign in with site credentials" button in `GetStartedViewController` when landing in the screen after entering site address - /// Used to enable sign-in to self-hosted sites using WordPress.org credentials. - /// Disabled by default - let enableSiteCredentialsLoginForSelfHostedSites: Bool - - /// If enabled, we will ask for WPCOM login after signing in using .org site credentials. - /// Disabled by default - let isWPComLoginRequiredForSiteCredentialsLogin: Bool - - /// If enabled, a magic link is sent automatically in place of password then fall back to password. - /// If disabled, password is shown by default with an option to send a magic link. - let isWPComMagicLinkPreferredToPassword: Bool - - /// If enabled, the alternative magic link action on the password screen is shown as a secondary call-to-action at the bottom. - /// If disabled, the alternative magic link action on the password screen is shown below the reset password action. - let isWPComMagicLinkShownAsSecondaryActionOnPasswordScreen: Bool - - /// If enabled, the Prologue screen will display only the entry point for WPCom login and no site address login. - /// - let enableWPComLoginOnlyInPrologue: Bool - - /// If enabled, an entry point to the site creation flow will be added to the bottom button of the prologue screen of simplified login. - /// - let enableSiteCreation: Bool - - /// If enabled, social login will be display at the bottom of the WPCom login screen. - /// - let enableSocialLogin: Bool - - /// If enabled, there will be a border around the email label on the WPCom password screen. - /// - let emphasizeEmailForWPComPassword: Bool - - /// The optional instructions for WPCom password. - /// - let wpcomPasswordInstructions: String? - - /// If enabled, site discovery will not check for XMLRPC URL. - /// - let skipXMLRPCCheckForSiteDiscovery: Bool - - /// If enabled, site address login will not check for XMLRPC URL. - /// - let skipXMLRPCCheckForSiteAddressLogin: Bool - - /// If enabled, the library will trigger the delegate method `handleSiteCredentialLogin` - /// instead of using the XMLRPC API for handling site credential login. - let enableManualSiteCredentialLogin: Bool - - /// If enabled, the library will not show any alert or inline error message - /// when site credential login fails. - /// Instead, the delegate method `handleSiteCredentialLoginFailure` will be called. - /// - let enableManualErrorHandlingForSiteCredentialLogin: Bool - - /// Used to determine the `step` value for `unified_login_step` analytics event in `GetStartedViewController` - /// - /// - If disabled `start` will be used as `step` value - /// - Disabled by default - /// - If enabled, `enter_email_address` will be used as `step` value - /// - Custom step value is used because `start` is used in other VCs as well, which doesn't allow us to differentiate between screens. - /// - i.e. Some screens have the same `step` and `flow` value. `GetStartedViewController` and `SiteAddressViewController` for example. - /// - let useEnterEmailAddressAsStepValueForGetStartedVC: Bool - - /// If enabled, the prologue screen should hide the WPCom login CTA and show only the entry point to site address login. - /// - let enableSiteAddressLoginOnlyInPrologue: Bool - - /// If enabled, the prologue screen would display a link for site creation guide. - /// - let enableSiteCreationGuide: Bool - - let disableAutofill: Bool - - /// Designated Initializer - /// - public init (wpcomClientId: String, - wpcomSecret: String, - wpcomScheme: String, - wpcomTermsOfServiceURL: URL, - wpcomBaseURL: URL = WordPressComOAuthClient.WordPressComOAuthDefaultBaseURL, - wpcomAPIBaseURL: URL = WordPressComOAuthClient.WordPressComOAuthDefaultApiBaseURL, - whatIsWPComURL: URL? = nil, - googleLoginClientId: String, - googleLoginServerClientId: String, - googleLoginScheme: String, - userAgent: String, - showLoginOptions: Bool = false, - enableSignUp: Bool = true, - enableSignInWithApple: Bool = false, - enableSignupWithGoogle: Bool = false, - enableUnifiedAuth: Bool = false, - enableUnifiedCarousel: Bool = false, - enablePasskeys: Bool = true, - displayHintButtons: Bool = true, - continueWithSiteAddressFirst: Bool = false, - enableSiteCredentialsLoginForSelfHostedSites: Bool = false, - isWPComLoginRequiredForSiteCredentialsLogin: Bool = false, - isWPComMagicLinkPreferredToPassword: Bool = false, - isWPComMagicLinkShownAsSecondaryActionOnPasswordScreen: Bool = false, - enableWPComLoginOnlyInPrologue: Bool = false, - enableSiteCreation: Bool = false, - enableSocialLogin: Bool = false, - emphasizeEmailForWPComPassword: Bool = false, - wpcomPasswordInstructions: String? = nil, - skipXMLRPCCheckForSiteDiscovery: Bool = false, - skipXMLRPCCheckForSiteAddressLogin: Bool = false, - enableManualSiteCredentialLogin: Bool = false, - enableManualErrorHandlingForSiteCredentialLogin: Bool = false, - useEnterEmailAddressAsStepValueForGetStartedVC: Bool = false, - enableSiteAddressLoginOnlyInPrologue: Bool = false, - enableSiteCreationGuide: Bool = false, - disableAutofill: Bool = false - ) { - - self.wpcomClientId = wpcomClientId - self.wpcomSecret = wpcomSecret - self.wpcomScheme = wpcomScheme - self.wpcomTermsOfServiceURL = wpcomTermsOfServiceURL - self.wpcomBaseURL = wpcomBaseURL - self.wpcomAPIBaseURL = wpcomAPIBaseURL - self.whatIsWPComURL = whatIsWPComURL - self.googleLoginClientId = googleLoginClientId - self.googleLoginServerClientId = googleLoginServerClientId - self.googleLoginScheme = googleLoginScheme - self.userAgent = userAgent - self.showLoginOptions = showLoginOptions - self.enableSignUp = enableSignUp - self.enableSignInWithApple = enableSignInWithApple - self.enableUnifiedAuth = enableUnifiedAuth - self.enableUnifiedCarousel = enableUnifiedCarousel - self.enablePasskeys = enablePasskeys - self.displayHintButtons = displayHintButtons - self.enableSignupWithGoogle = enableSignupWithGoogle - self.continueWithSiteAddressFirst = continueWithSiteAddressFirst - self.enableSiteCredentialsLoginForSelfHostedSites = enableSiteCredentialsLoginForSelfHostedSites - self.isWPComLoginRequiredForSiteCredentialsLogin = isWPComLoginRequiredForSiteCredentialsLogin - self.isWPComMagicLinkPreferredToPassword = isWPComMagicLinkPreferredToPassword - self.isWPComMagicLinkShownAsSecondaryActionOnPasswordScreen = isWPComMagicLinkShownAsSecondaryActionOnPasswordScreen - self.enableWPComLoginOnlyInPrologue = enableWPComLoginOnlyInPrologue - self.enableSiteCreation = enableSiteCreation - self.enableSocialLogin = enableSocialLogin - self.emphasizeEmailForWPComPassword = emphasizeEmailForWPComPassword - self.wpcomPasswordInstructions = wpcomPasswordInstructions - self.skipXMLRPCCheckForSiteDiscovery = skipXMLRPCCheckForSiteDiscovery - self.skipXMLRPCCheckForSiteAddressLogin = skipXMLRPCCheckForSiteAddressLogin - self.enableManualSiteCredentialLogin = enableManualSiteCredentialLogin - self.enableManualErrorHandlingForSiteCredentialLogin = enableManualErrorHandlingForSiteCredentialLogin - self.useEnterEmailAddressAsStepValueForGetStartedVC = useEnterEmailAddressAsStepValueForGetStartedVC - self.enableSiteAddressLoginOnlyInPrologue = enableSiteAddressLoginOnlyInPrologue - self.enableSiteCreationGuide = enableSiteCreationGuide - self.disableAutofill = disableAutofill - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDelegateProtocol.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDelegateProtocol.swift deleted file mode 100644 index 5c2724a8cd6c..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDelegateProtocol.swift +++ /dev/null @@ -1,198 +0,0 @@ -import Foundation -import WordPressShared - -// MARK: - WordPressAuthenticator Delegate Protocol -// -public protocol WordPressAuthenticatorDelegate: AnyObject { - - /// Indicates if the active Authenticator can be dismissed, or not. - /// - var dismissActionEnabled: Bool { get } - - /// Indicates if the Support button action should be enabled, or not. - /// - var supportActionEnabled: Bool { get } - - /// Indicates if the WordPress.com's Terms of Service should be enabled, or not. - /// - var wpcomTermsOfServiceEnabled: Bool { get } - - /// Indicates if Support is available or not. - /// - var supportEnabled: Bool { get } - - /// Returns true if there isn't a default WordPress.com account connected in the app. - var allowWPComLogin: Bool { get } - - /// Signals the Host App that a new WordPress.com account has just been created. - /// - /// - Parameters: - /// - username: WordPress.com Username. - /// - authToken: WordPress.com Bearer Token. - /// - func createdWordPressComAccount(username: String, authToken: String) - - /// Signals the Host App that the user has successfully authenticated with an Apple account. - /// - /// - Parameters: - /// - appleUserID: User ID received in the Apple credentials. - /// - func userAuthenticatedWithAppleUserID(_ appleUserID: String) - - /// Presents the Support new request, from a given ViewController, with a specified SourceTag. - /// - func presentSupportRequest(from sourceViewController: UIViewController, sourceTag: WordPressSupportSourceTag) - - /// Signals to the Host App that a WordPress site is available and needs validated - /// before presenting the username and password view controller. - /// - Parameters: - /// - site: passes in the site information to the delegate method. - /// - onCompletion: Closure to be executed on completion. - /// - func shouldPresentUsernamePasswordController(for siteInfo: WordPressComSiteInfo?, onCompletion: @escaping (WordPressAuthenticatorResult) -> Void) - - /// Presents the Login Epilogue, in the specified NavigationController. - /// - /// - Parameters: - /// - navigationController: navigation stack for any epilogue views to be shown on. - /// - credentials: WPCOM or WPORG credentials. - /// - source: an optional identifier of the login flow, can be from the login prologue or provided by the host app. - /// - onDismiss: called when the auth flow is dismissed. - func presentLoginEpilogue(in navigationController: UINavigationController, for credentials: AuthenticatorCredentials, source: SignInSource?, onDismiss: @escaping () -> Void) - - /// Presents the Login Epilogue, in the specified NavigationController. - /// - func presentSignupEpilogue( - in navigationController: UINavigationController, - for credentials: AuthenticatorCredentials, - socialUser: SocialUser? - ) - - /// Presents the Support Interface from a given ViewController. - /// - /// - Parameters: - /// - from: ViewController from which to present the support interface from - /// - sourceTag: Support source tag of the view controller. - /// - lastStep: Last `Step` tracked in `AuthenticatorAnalyticsTracker` - /// - lastFlow: Last `Flow` tracked in `AuthenticatorAnalyticsTracker` - /// - func presentSupport(from sourceViewController: UIViewController, sourceTag: WordPressSupportSourceTag, lastStep: AuthenticatorAnalyticsTracker.Step, lastFlow: AuthenticatorAnalyticsTracker.Flow) - - /// Indicates if the Login Epilogue should be displayed. - /// - /// - Parameter isJetpackLogin: Indicates if we've just logged into a WordPress.com account for Jetpack purposes!. - /// - func shouldPresentLoginEpilogue(isJetpackLogin: Bool) -> Bool - - /// Indicates the Host app wants to handle and display a given error. - /// - func shouldHandleError(_ error: Error) -> Bool - - /// Signals the Host app that there is an error that needs to be handled. - /// - func handleError(_ error: Error, onCompletion: @escaping (UIViewController) -> Void) - - /// Indicates if the Signup Epilogue should be displayed. - /// - func shouldPresentSignupEpilogue() -> Bool - - /// Signals the Host App that a WordPress Site (wpcom or wporg) is available with the specified credentials. - /// - /// - Parameters: - /// - credentials: WordPress Site Credentials. - /// - onCompletion: Closure to be executed on completion. - /// - func sync(credentials: AuthenticatorCredentials, onCompletion: @escaping () -> Void) - - /// Signals to the Host App that a WordPress site is available and needs validated. - /// This method is only triggered in the site discovery flow. - /// - /// - Parameters: - /// - siteInfo: The fetched site information - can be nil the site doesn't exist or have WordPress - /// - navigationController: the current navigation stack of the site discovery flow. - /// - func troubleshootSite(_ siteInfo: WordPressComSiteInfo?, in navigationController: UINavigationController?) - - /// Sends site credentials to the host app so that it can handle login locally. - /// This method is only triggered when the config `skipXMLRPCCheckForSiteAddressLogin` is enabled. - /// - /// - Parameters: - /// - credentials: WordPress.org credentials submitted in the site credentials form. - /// - onLoading: the block to update the loading state on the site credentials form when necessary. - /// - onSuccess: the block to finish the login flow after login succeeds. - /// - onFailure: the block to trigger error handling. The closure accepts an error and a boolean indicating if the login failed with incorrect credentials. - /// - func handleSiteCredentialLogin(credentials: WordPressOrgCredentials, - onLoading: @escaping (Bool) -> Void, - onSuccess: @escaping () -> Void, - onFailure: @escaping (Error, Bool) -> Void) - - /// Signals to the Host App to handle an error for site credential login. - /// - /// - Parameters: - /// - error: The site credential login failure. - /// - siteURL: The site URL of the login failure. - /// - viewController: the view controller containing the site credential input. - /// - func handleSiteCredentialLoginFailure(error: Error, - for siteURL: String, - in viewController: UIViewController) - - /// Signals to the Host App to navigate to the site creation flow. - /// This method is currently used only in the simplified login flow - /// when the configs `enableSimplifiedLoginI1` and `enableSiteCreationForSimplifiedLoginI1` is enabled - /// - /// - Parameters: - /// - navigationController: the current navigation stack of the login flow. - /// - func showSiteCreation(in navigationController: UINavigationController) - - /// Signals to the Host App to navigate to the site creation guide. - /// This method triggered only if `enableSiteCreationGuide` config is enabled. - /// - /// - Parameters: - /// - navigationController: the current navigation stack of the login flow. - /// - func showSiteCreationGuide(in navigationController: UINavigationController) - - /// Signals the Host App that a given Analytics Event has occurred. - /// - func track(event: WPAnalyticsStat) - - /// Signals the Host App that a given Analytics Event (with the specified properties) has occurred. - /// - func track(event: WPAnalyticsStat, properties: [AnyHashable: Any]) - - /// Signals the Host App that a given Analytics Event (with an associated Error) has occurred. - /// - func track(event: WPAnalyticsStat, error: Error) -} - -/// Extension with default implementation for optional delegate methods. -/// -public extension WordPressAuthenticatorDelegate { - func troubleshootSite(_ siteInfo: WordPressComSiteInfo?, in navigationController: UINavigationController?) { - // No-op - } - - func showSiteCreation(in navigationController: UINavigationController) { - // No-op - } - - func showSiteCreationGuide(in navigationController: UINavigationController) { - // No-op - } - - func handleSiteCredentialLogin(credentials: WordPressOrgCredentials, - onLoading: @escaping (Bool) -> Void, - onSuccess: @escaping () -> Void, - onFailure: @escaping (Error, Bool) -> Void) { - // No-op - } - - func handleSiteCredentialLoginFailure(error: Error, - for siteURL: String, - in viewController: UIViewController) { - // No-op - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDisplayImages.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDisplayImages.swift deleted file mode 100644 index 97c38fd3ab7f..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDisplayImages.swift +++ /dev/null @@ -1,19 +0,0 @@ -// MARK: - WordPress Authenticator Display Images -// -public struct WordPressAuthenticatorDisplayImages { - public let magicLink: UIImage - - /// Designated initializer. - /// - public init(magicLink: UIImage) { - self.magicLink = magicLink - } -} - -public extension WordPressAuthenticatorDisplayImages { - static var defaultImages: WordPressAuthenticatorDisplayImages { - return WordPressAuthenticatorDisplayImages( - magicLink: .magicLinkImage - ) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDisplayStrings.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDisplayStrings.swift deleted file mode 100644 index a160b331fee9..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDisplayStrings.swift +++ /dev/null @@ -1,258 +0,0 @@ -import Foundation - -// MARK: - WordPress Authenticator Display Strings -// -public struct WordPressAuthenticatorDisplayStrings { - /// Strings: Login instructions. - /// - public let emailLoginInstructions: String - public let getStartedInstructions: String - public let jetpackLoginInstructions: String - public let siteLoginInstructions: String - public let siteCredentialInstructions: String - public let usernamePasswordInstructions: String - public let twoFactorInstructions: String - public let twoFactorOtherFormsInstructions: String - public let magicLinkSignupInstructions: String - public let openMailSignupInstructions: String - public let openMailLoginInstructions: String - public let verifyMailLoginInstructions: String - public let alternativelyEnterPasswordInstructions: String - public let checkSpamInstructions: String - public let oopsInstructions: String - public let googleSignupInstructions: String - public let googlePasswordInstructions: String - public let applePasswordInstructions: String - - /// Strings: primary call-to-action button titles. - /// - public let continueButtonTitle: String - public let magicLinkButtonTitle: String - public let openMailButtonTitle: String - public let createAccountButtonTitle: String - public let continueWithWPButtonTitle: String - public let enterYourSiteAddressButtonTitle: String - public let signInWithSiteCredentialsButtonTitle: String - public let sendEmailVerificationLinkButtonTitle: String - public let loginWithAccountPasswordButtonTitle: String - - /// Large titles displayed in unified auth flows. - /// - public let getStartedTitle: String - public let logInTitle: String - public let signUpTitle: String - public let waitingForGoogleTitle: String - - /// Strings: secondary call-to-action button titles. - /// - public let findSiteButtonTitle: String - public let resetPasswordButtonTitle: String - public let getLoginLinkButtonTitle: String - public let textCodeButtonTitle: String - public let securityKeyButtonTitle: String - public let loginTermsOfService: String - public let signupTermsOfService: String - public let whatIsWPComLinkTitle: String - public let siteCreationButtonTitle: String - public let siteCreationGuideButtonTitle: String - - /// Placeholder text for textfields. - /// - public let usernamePlaceholder: String - public let passwordPlaceholder: String - public let siteAddressPlaceholder: String - public let twoFactorCodePlaceholder: String - public let emailAddressPlaceholder: String - - /// Designated initializer. - /// - public init(emailLoginInstructions: String = defaultStrings.emailLoginInstructions, - getStartedInstructions: String = defaultStrings.getStartedInstructions, - jetpackLoginInstructions: String = defaultStrings.jetpackLoginInstructions, - siteLoginInstructions: String = defaultStrings.siteLoginInstructions, - siteCredentialInstructions: String = defaultStrings.siteCredentialInstructions, - usernamePasswordInstructions: String = defaultStrings.usernamePasswordInstructions, - twoFactorInstructions: String = defaultStrings.twoFactorInstructions, - twoFactorOtherFormsInstructions: String = defaultStrings.twoFactorOtherFormsInstructions, - magicLinkSignupInstructions: String = defaultStrings.magicLinkSignupInstructions, - openMailSignupInstructions: String = defaultStrings.openMailSignupInstructions, - openMailLoginInstructions: String = defaultStrings.openMailLoginInstructions, - verifyMailLoginInstructions: String = defaultStrings.verifyMailLoginInstructions, - alternativelyEnterPasswordInstructions: String = defaultStrings.alternativelyEnterPasswordInstructions, - checkSpamInstructions: String = defaultStrings.checkSpamInstructions, - oopsInstructions: String = defaultStrings.oopsInstructions, - googleSignupInstructions: String = defaultStrings.googleSignupInstructions, - googlePasswordInstructions: String = defaultStrings.googlePasswordInstructions, - applePasswordInstructions: String = defaultStrings.applePasswordInstructions, - continueButtonTitle: String = defaultStrings.continueButtonTitle, - magicLinkButtonTitle: String = defaultStrings.magicLinkButtonTitle, - openMailButtonTitle: String = defaultStrings.openMailButtonTitle, - createAccountButtonTitle: String = defaultStrings.createAccountButtonTitle, - continueWithWPButtonTitle: String = defaultStrings.continueWithWPButtonTitle, - enterYourSiteAddressButtonTitle: String = defaultStrings.enterYourSiteAddressButtonTitle, - signInWithSiteCredentialsButtonTitle: String = defaultStrings.signInWithSiteCredentialsButtonTitle, - sendEmailVerificationLinkButtonTitle: String = defaultStrings.sendEmailVerificationLinkButtonTitle, - loginWithAccountPasswordButtonTitle: String = defaultStrings.loginWithAccountPasswordButtonTitle, - findSiteButtonTitle: String = defaultStrings.findSiteButtonTitle, - resetPasswordButtonTitle: String = defaultStrings.resetPasswordButtonTitle, - getLoginLinkButtonTitle: String = defaultStrings.getLoginLinkButtonTitle, - textCodeButtonTitle: String = defaultStrings.textCodeButtonTitle, - securityKeyButtonTitle: String = defaultStrings.securityKeyButtonTitle, - loginTermsOfService: String = defaultStrings.loginTermsOfService, - signupTermsOfService: String = defaultStrings.signupTermsOfService, - whatIsWPComLinkTitle: String = defaultStrings.whatIsWPComLinkTitle, - siteCreationButtonTitle: String = defaultStrings.siteCreationButtonTitle, - getStartedTitle: String = defaultStrings.getStartedTitle, - logInTitle: String = defaultStrings.logInTitle, - signUpTitle: String = defaultStrings.signUpTitle, - waitingForGoogleTitle: String = defaultStrings.waitingForGoogleTitle, - usernamePlaceholder: String = defaultStrings.usernamePlaceholder, - passwordPlaceholder: String = defaultStrings.passwordPlaceholder, - siteAddressPlaceholder: String = defaultStrings.siteAddressPlaceholder, - twoFactorCodePlaceholder: String = defaultStrings.twoFactorCodePlaceholder, - emailAddressPlaceholder: String = defaultStrings.emailAddressPlaceholder, - siteCreationGuideButtonTitle: String = defaultStrings.siteCreationGuideButtonTitle) { - self.emailLoginInstructions = emailLoginInstructions - self.getStartedInstructions = getStartedInstructions - self.jetpackLoginInstructions = jetpackLoginInstructions - self.siteLoginInstructions = siteLoginInstructions - self.siteCredentialInstructions = siteCredentialInstructions - self.usernamePasswordInstructions = usernamePasswordInstructions - self.twoFactorInstructions = twoFactorInstructions - self.twoFactorOtherFormsInstructions = twoFactorOtherFormsInstructions - self.magicLinkSignupInstructions = magicLinkSignupInstructions - self.openMailSignupInstructions = openMailSignupInstructions - self.openMailLoginInstructions = openMailLoginInstructions - self.verifyMailLoginInstructions = verifyMailLoginInstructions - self.alternativelyEnterPasswordInstructions = alternativelyEnterPasswordInstructions - self.checkSpamInstructions = checkSpamInstructions - self.oopsInstructions = oopsInstructions - self.googleSignupInstructions = googleSignupInstructions - self.googlePasswordInstructions = googlePasswordInstructions - self.applePasswordInstructions = applePasswordInstructions - self.continueButtonTitle = continueButtonTitle - self.magicLinkButtonTitle = magicLinkButtonTitle - self.openMailButtonTitle = openMailButtonTitle - self.createAccountButtonTitle = createAccountButtonTitle - self.continueWithWPButtonTitle = continueWithWPButtonTitle - self.enterYourSiteAddressButtonTitle = enterYourSiteAddressButtonTitle - self.signInWithSiteCredentialsButtonTitle = signInWithSiteCredentialsButtonTitle - self.sendEmailVerificationLinkButtonTitle = sendEmailVerificationLinkButtonTitle - self.loginWithAccountPasswordButtonTitle = loginWithAccountPasswordButtonTitle - self.findSiteButtonTitle = findSiteButtonTitle - self.resetPasswordButtonTitle = resetPasswordButtonTitle - self.getLoginLinkButtonTitle = getLoginLinkButtonTitle - self.textCodeButtonTitle = textCodeButtonTitle - self.securityKeyButtonTitle = securityKeyButtonTitle - self.loginTermsOfService = loginTermsOfService - self.signupTermsOfService = signupTermsOfService - self.whatIsWPComLinkTitle = whatIsWPComLinkTitle - self.siteCreationButtonTitle = siteCreationButtonTitle - self.getStartedTitle = getStartedTitle - self.logInTitle = logInTitle - self.signUpTitle = signUpTitle - self.waitingForGoogleTitle = waitingForGoogleTitle - self.usernamePlaceholder = usernamePlaceholder - self.passwordPlaceholder = passwordPlaceholder - self.siteAddressPlaceholder = siteAddressPlaceholder - self.twoFactorCodePlaceholder = twoFactorCodePlaceholder - self.emailAddressPlaceholder = emailAddressPlaceholder - self.siteCreationGuideButtonTitle = siteCreationGuideButtonTitle - } -} - -public extension WordPressAuthenticatorDisplayStrings { - static var defaultStrings: WordPressAuthenticatorDisplayStrings { - return WordPressAuthenticatorDisplayStrings( - emailLoginInstructions: NSLocalizedString("Log in to your WordPress.com account with your email address.", - comment: "Instruction text on the login's email address screen."), - getStartedInstructions: NSLocalizedString("Enter your email address to log in or create a WordPress.com account.", - comment: "Instruction text on the initial email address entry screen."), - jetpackLoginInstructions: NSLocalizedString("Log in to the WordPress.com account you used to connect Jetpack.", - comment: "Instruction text on the login's email address screen."), - siteLoginInstructions: NSLocalizedString("Enter the address of the WordPress site you'd like to connect.", - comment: "Instruction text on the login's site addresss screen."), - siteCredentialInstructions: NSLocalizedString("Enter your account information for %@.", - comment: "Enter your account information for {site url}. Asks the user to enter a username and password for their self-hosted site."), - usernamePasswordInstructions: NSLocalizedString("Log in with your WordPress.com username and password.", - comment: "Instructions on the WordPress.com username / password log in form."), - twoFactorInstructions: NSLocalizedString("login.twoFactorInstructions.details", value: "Please enter the verification code from your authentication app for your WordPress.com account.", comment: "Instruction label in the two-factor authorization screen WordPress.com login authentication. Note: it has to mention that it's for a WordPress.com account."), - twoFactorOtherFormsInstructions: NSLocalizedString("Or choose another form of authentication.", - comment: "Instruction text for other forms of two-factor auth methods."), - magicLinkSignupInstructions: NSLocalizedString("We'll email you a signup link to create your new WordPress.com account.", - comment: "Instruction text on the Sign Up screen."), - openMailSignupInstructions: NSLocalizedString("We've emailed you a signup link to create your new WordPress.com account. Check your email on this device, and tap the link in the email you receive from WordPress.com.", - comment: "Instruction text after a signup Magic Link was requested."), - openMailLoginInstructions: NSLocalizedString("Check your email on this device, and tap the link in the email you receive from WordPress.com.", - comment: "Instruction text after a login Magic Link was requested."), - verifyMailLoginInstructions: NSLocalizedString("A WordPress.com account is connected to your store credentials. To continue, we will send a verification link to the email address above.", - comment: "Instruction text to explain magic link login step."), - alternativelyEnterPasswordInstructions: NSLocalizedString("Alternatively, you may enter the password for this account.", - comment: "Instruction text to explain to help users type their password instead of using magic link login option."), - checkSpamInstructions: NSLocalizedString("Not seeing the email? Check your Spam or Junk Mail folder.", comment: "Instructions after a Magic Link was sent, but the email can't be found in their inbox."), - oopsInstructions: NSLocalizedString("Didn't mean to create a new account? Go back to re-enter your email address.", comment: "Instructions after a Magic Link was sent, but email is incorrect."), - googleSignupInstructions: NSLocalizedString("We'll use this email address to create your new WordPress.com account.", comment: "Text confirming email address to be used for new account."), - googlePasswordInstructions: NSLocalizedString("To proceed with this Google account, please first log in with your WordPress.com password. This will only be asked once.", - comment: "Instructional text shown when requesting the user's password for Google login."), - applePasswordInstructions: NSLocalizedString("To proceed with this Apple ID, please first log in with your WordPress.com password. This will only be asked once.", - comment: "Instructional text shown when requesting the user's password for Apple login."), - continueButtonTitle: NSLocalizedString("Continue", - comment: "The button title text when there is a next step for logging in or signing up."), - magicLinkButtonTitle: NSLocalizedString("Send Link by Email", - comment: "The button title text for sending a magic link."), - openMailButtonTitle: NSLocalizedString("Open Mail", - comment: "The button title text for opening the user's preferred email app."), - createAccountButtonTitle: NSLocalizedString("Create Account", - comment: "The button title text for creating a new account."), - continueWithWPButtonTitle: NSLocalizedString("Log in or sign up with WordPress.com", - comment: "Button title. Takes the user to the login by email flow."), - enterYourSiteAddressButtonTitle: NSLocalizedString("Enter your existing site address", - comment: "Button title. Takes the user to the login by site address flow."), - signInWithSiteCredentialsButtonTitle: NSLocalizedString("Sign in with site credentials", - comment: "Button title. Takes the user the Enter site credentials screen."), - sendEmailVerificationLinkButtonTitle: NSLocalizedString("Send email verification link", - comment: "Button title. Sends a email verification link (Magin link) for signing in."), - loginWithAccountPasswordButtonTitle: NSLocalizedString("Login with account password", - comment: "Button title. Takes the user to the Enter account password screen."), - findSiteButtonTitle: NSLocalizedString("Find your site address", - comment: "The hint button's title text to help users find their site address."), - resetPasswordButtonTitle: NSLocalizedString("Reset your password", - comment: "The button title for a secondary call-to-action button. When the user can't remember their password."), - getLoginLinkButtonTitle: NSLocalizedString("Get a login link by email", - comment: "The button title for a secondary call-to-action button. When the user wants to try sending a magic link instead of entering a password."), - textCodeButtonTitle: NSLocalizedString("Text me a code via SMS", - comment: "The button's title text to send a 2FA code via SMS text message."), - securityKeyButtonTitle: NSLocalizedString("Use a security key", - comment: "The button's title text to use a security key."), - loginTermsOfService: NSLocalizedString("By continuing, you agree to our _Terms of Service_.", comment: "Legal disclaimer for logging in. The underscores _..._ denote underline."), - signupTermsOfService: NSLocalizedString("If you continue with Apple or Google and don't already have a WordPress.com account, you are creating an account and you agree to our _Terms of Service_.", comment: "Legal disclaimer for signing up. The underscores _..._ denote underline."), - whatIsWPComLinkTitle: NSLocalizedString("What is WordPress.com?", - comment: "Navigates to page with details about What is WordPress.com."), - siteCreationButtonTitle: NSLocalizedString("Create a Site", - comment: "Navigates to a new flow for site creation."), - getStartedTitle: NSLocalizedString("Get Started", - comment: "View title for initial auth views."), - logInTitle: NSLocalizedString("Log In", - comment: "View title during the log in process."), - signUpTitle: NSLocalizedString("Sign Up", - comment: "View title during the sign up process."), - waitingForGoogleTitle: NSLocalizedString("Waiting...", - comment: "View title during the Google auth process."), - usernamePlaceholder: NSLocalizedString("Username", - comment: "Placeholder for the username textfield."), - passwordPlaceholder: NSLocalizedString("Password", - comment: "Placeholder for the password textfield."), - siteAddressPlaceholder: NSLocalizedString("example.com", - comment: "Placeholder for the site url textfield."), - twoFactorCodePlaceholder: NSLocalizedString("Authentication code", - comment: "Placeholder for the 2FA code textfield."), - emailAddressPlaceholder: NSLocalizedString("Email address", - comment: "Placeholder for the email address textfield."), - siteCreationGuideButtonTitle: NSLocalizedString( - "wordPressAuthenticatorDisplayStrings.default.siteCreationGuideButtonTitle", - value: "Starting a new site?", - comment: "Title for the link for site creation guide." - ) - ) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorResult.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorResult.swift deleted file mode 100644 index e8497064271a..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorResult.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation - -/// Provides options for clients of WordPressAuthenticator -/// to signal what they expect WPAuthenticator to do in response to -/// `shouldPresentUsernamePasswordController` -/// -/// @see WordPressAuthenticatorDelegate.shouldPresentUsernamePasswordController -public enum WordPressAuthenticatorResult { - - /// An error - /// - case error(value: Error) - - /// Boolean flag to indicate if UI providing entry for username and passsword - /// should be presented - /// - case presentPasswordController(value: Bool) - - /// Present the view controller requesting the email address - /// associated to the user's wordpress.com account - /// - case presentEmailController - - /// A view controller to be inserted into the navigation stack - /// - case injectViewController(value: UIViewController) -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorStyles.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorStyles.swift deleted file mode 100644 index 3d9214100967..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorStyles.swift +++ /dev/null @@ -1,305 +0,0 @@ -import UIKit -import Gridicons -import WordPressShared - -// MARK: - WordPress Authenticator Styles -// -public struct WordPressAuthenticatorStyle { - /// Style: Primary + Normal State - /// - public let primaryNormalBackgroundColor: UIColor - - public let primaryNormalBorderColor: UIColor? - - /// Style: Primary + Highlighted State - /// - public let primaryHighlightBackgroundColor: UIColor - - public let primaryHighlightBorderColor: UIColor? - - /// Style: Secondary - /// - public let secondaryNormalBackgroundColor: UIColor - - public let secondaryNormalBorderColor: UIColor - - public let secondaryHighlightBackgroundColor: UIColor - - public let secondaryHighlightBorderColor: UIColor - - /// Style: Disabled State - /// - public let disabledBackgroundColor: UIColor - - public let disabledBorderColor: UIColor - - public let primaryTitleColor: UIColor - - public let secondaryTitleColor: UIColor - - public let disabledTitleColor: UIColor - - /// Color of the spinner that is shown when a button is disabled. - public let disabledButtonActivityIndicatorColor: UIColor - - /// Style: Text Buttons - /// - public let textButtonColor: UIColor - - public let textButtonHighlightColor: UIColor - - /// Style: Labels - /// - public let instructionColor: UIColor - - public let subheadlineColor: UIColor - - public let placeholderColor: UIColor - - /// Style: Login screen background colors - /// - public let viewControllerBackgroundColor: UIColor - - public let textFieldBackgroundColor: UIColor - - // If not specified, falls back to viewControllerBackgroundColor. - public let buttonViewBackgroundColor: UIColor - - /// Style: shadow image view on top of the button view like a divider. - /// If not specified, falls back to image "darkgrey-shadow". - /// - public let buttonViewTopShadowImage: UIImage? - - /// Style: nav bar - /// - public let navBarImage: UIImage - - public let navBarBadgeColor: UIColor - - public let navBarBackgroundColor: UIColor - - public let navButtonTextColor: UIColor - - /// Style: prologue background colors - /// - public let prologueBackgroundColor: UIColor - - /// Style: optional prologue background image - /// - public let prologueBackgroundImage: UIImage? - - /// Style: prologue background colors - /// - public let prologueTitleColor: UIColor - - /// Style: optional prologue buttons blur effect - public let prologueButtonsBlurEffect: UIBlurEffect? - - /// Style: primary button on the prologue view (continue) - /// When `nil` it will use the primary styles defined here - /// Defaults to `nil` - /// - public let prologuePrimaryButtonStyle: NUXButtonStyle? - - /// Style: secondary button on the prologue view (site address) - /// When `nil` it will use the secondary styles defined here - /// Defaults to `nil` - /// - public let prologueSecondaryButtonStyle: NUXButtonStyle? - - /// Style: prologue top container child view controller - /// When nil, `LoginProloguePageViewController` is displayed in the top container - /// - public let prologueTopContainerChildViewController: () -> UIViewController? - - /// Style: status bar style - /// - public let statusBarStyle: UIStatusBarStyle - - /// Style: OR divider separator color - /// - /// Used in `NUXStackedButtonsViewController` - /// - public let orDividerSeparatorColor: UIColor - - /// Style: OR divider text color - /// - /// Used in `NUXStackedButtonsViewController` - /// - public let orDividerTextColor: UIColor - - /// Designated initializer - /// - public init(primaryNormalBackgroundColor: UIColor, - primaryNormalBorderColor: UIColor?, - primaryHighlightBackgroundColor: UIColor, - primaryHighlightBorderColor: UIColor?, - secondaryNormalBackgroundColor: UIColor, - secondaryNormalBorderColor: UIColor, - secondaryHighlightBackgroundColor: UIColor, - secondaryHighlightBorderColor: UIColor, - disabledBackgroundColor: UIColor, - disabledBorderColor: UIColor, - primaryTitleColor: UIColor, - secondaryTitleColor: UIColor, - disabledTitleColor: UIColor, - disabledButtonActivityIndicatorColor: UIColor, - textButtonColor: UIColor, - textButtonHighlightColor: UIColor, - instructionColor: UIColor, - subheadlineColor: UIColor, - placeholderColor: UIColor, - viewControllerBackgroundColor: UIColor, - textFieldBackgroundColor: UIColor, - buttonViewBackgroundColor: UIColor? = nil, - buttonViewTopShadowImage: UIImage? = UIImage(named: "darkgrey-shadow"), - navBarImage: UIImage, - navBarBadgeColor: UIColor, - navBarBackgroundColor: UIColor, - navButtonTextColor: UIColor = .white, - prologueBackgroundColor: UIColor = WPStyleGuide.wordPressBlue(), - prologueBackgroundImage: UIImage? = nil, - prologueTitleColor: UIColor = .white, - prologueButtonsBlurEffect: UIBlurEffect? = nil, - prologuePrimaryButtonStyle: NUXButtonStyle? = nil, - prologueSecondaryButtonStyle: NUXButtonStyle? = nil, - prologueTopContainerChildViewController: @autoclosure @escaping () -> UIViewController? = nil, - statusBarStyle: UIStatusBarStyle = .lightContent, - orDividerSeparatorColor: UIColor = .tertiaryLabel, - orDividerTextColor: UIColor = .secondaryLabel) { - self.primaryNormalBackgroundColor = primaryNormalBackgroundColor - self.primaryNormalBorderColor = primaryNormalBorderColor - self.primaryHighlightBackgroundColor = primaryHighlightBackgroundColor - self.primaryHighlightBorderColor = primaryHighlightBorderColor - self.secondaryNormalBackgroundColor = secondaryNormalBackgroundColor - self.secondaryNormalBorderColor = secondaryNormalBorderColor - self.secondaryHighlightBackgroundColor = secondaryHighlightBackgroundColor - self.secondaryHighlightBorderColor = secondaryHighlightBorderColor - self.disabledBackgroundColor = disabledBackgroundColor - self.disabledBorderColor = disabledBorderColor - self.primaryTitleColor = primaryTitleColor - self.secondaryTitleColor = secondaryTitleColor - self.disabledTitleColor = disabledTitleColor - self.disabledButtonActivityIndicatorColor = disabledButtonActivityIndicatorColor - self.textButtonColor = textButtonColor - self.textButtonHighlightColor = textButtonHighlightColor - self.instructionColor = instructionColor - self.subheadlineColor = subheadlineColor - self.placeholderColor = placeholderColor - self.viewControllerBackgroundColor = viewControllerBackgroundColor - self.textFieldBackgroundColor = textFieldBackgroundColor - self.buttonViewBackgroundColor = buttonViewBackgroundColor ?? viewControllerBackgroundColor - self.buttonViewTopShadowImage = buttonViewTopShadowImage - self.navBarImage = navBarImage - self.navBarBadgeColor = navBarBadgeColor - self.navBarBackgroundColor = navBarBackgroundColor - self.navButtonTextColor = navButtonTextColor - self.prologueBackgroundColor = prologueBackgroundColor - self.prologueBackgroundImage = prologueBackgroundImage - self.prologueTitleColor = prologueTitleColor - self.prologueButtonsBlurEffect = prologueButtonsBlurEffect - self.prologuePrimaryButtonStyle = prologuePrimaryButtonStyle - self.prologueSecondaryButtonStyle = prologueSecondaryButtonStyle - self.prologueTopContainerChildViewController = prologueTopContainerChildViewController - self.statusBarStyle = statusBarStyle - self.orDividerSeparatorColor = orDividerSeparatorColor - self.orDividerTextColor = orDividerTextColor - } -} - -// MARK: - WordPress Unified Authenticator Styles -// -// Styles specifically for the unified auth flows. -// -public struct WordPressAuthenticatorUnifiedStyle { - - /// Style: Auth view border colors - /// - public let borderColor: UIColor - - /// Style Auth default error color - /// - public let errorColor: UIColor - - /// Style: Auth default text color - /// - public let textColor: UIColor - - /// Style: Auth subtle text color - /// - public let textSubtleColor: UIColor - - /// Style: Auth plain text button normal state color - /// - public let textButtonColor: UIColor - - /// Style: Auth plain text button highlight state color - /// - public let textButtonHighlightColor: UIColor - - /// Style: Auth view background colors - /// - public let viewControllerBackgroundColor: UIColor - - /// Style: Auth Prologue buttons background color - public let prologueButtonsBackgroundColor: UIColor - - /// Style: Auth Prologue view background color - public let prologueViewBackgroundColor: UIColor - - /// Style: optional auth Prologue view background image - public let prologueBackgroundImage: UIImage? - - /// Style: optional blur effect for the buttons view - public let prologueButtonsBlurEffect: UIBlurEffect? - - /// Style: Status bar style. Defaults to `default`. - /// - public let statusBarStyle: UIStatusBarStyle - - /// Style: Navigation bar. - /// - public let navBarBackgroundColor: UIColor - public let navButtonTextColor: UIColor - public let navTitleTextColor: UIColor - - /// Style: Text color to be used for email in `GravatarEmailTableViewCell` - /// - public let gravatarEmailTextColor: UIColor? - - /// Designated initializer - /// - public init(borderColor: UIColor, - errorColor: UIColor, - textColor: UIColor, - textSubtleColor: UIColor, - textButtonColor: UIColor, - textButtonHighlightColor: UIColor, - viewControllerBackgroundColor: UIColor, - prologueButtonsBackgroundColor: UIColor = .clear, - prologueViewBackgroundColor: UIColor? = nil, - prologueBackgroundImage: UIImage? = nil, - prologueButtonsBlurEffect: UIBlurEffect? = nil, - statusBarStyle: UIStatusBarStyle = .default, - navBarBackgroundColor: UIColor, - navButtonTextColor: UIColor, - navTitleTextColor: UIColor, - gravatarEmailTextColor: UIColor? = nil) { - self.borderColor = borderColor - self.errorColor = errorColor - self.textColor = textColor - self.textSubtleColor = textSubtleColor - self.textButtonColor = textButtonColor - self.textButtonHighlightColor = textButtonHighlightColor - self.viewControllerBackgroundColor = viewControllerBackgroundColor - self.prologueButtonsBackgroundColor = prologueButtonsBackgroundColor - self.prologueViewBackgroundColor = prologueViewBackgroundColor ?? viewControllerBackgroundColor - self.prologueBackgroundImage = prologueBackgroundImage - self.prologueButtonsBlurEffect = prologueButtonsBlurEffect - self.statusBarStyle = statusBarStyle - self.navBarBackgroundColor = navBarBackgroundColor - self.navButtonTextColor = navButtonTextColor - self.navTitleTextColor = navTitleTextColor - self.gravatarEmailTextColor = gravatarEmailTextColor - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressSupportSourceTag.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressSupportSourceTag.swift deleted file mode 100644 index 6df18dee9e76..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressSupportSourceTag.swift +++ /dev/null @@ -1,84 +0,0 @@ -import Foundation - -// MARK: - Authentication Flow Event. Useful to relay internal Auth events over to activity trackers. -// -public struct WordPressSupportSourceTag { - public let name: String - public let origin: String? - - public init(name: String, origin: String? = nil) { - self.name = name - self.origin = origin - } -} - -func ==(lhs: WordPressSupportSourceTag, rhs: WordPressSupportSourceTag) -> Bool { - return lhs.name == rhs.name -} - -extension WordPressSupportSourceTag { - public static var generalLogin: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "generalLogin", origin: "origin:login-screen") - } - public static var jetpackLogin: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "jetpackLogin", origin: "origin:jetpack-login-screen") - } - public static var loginEmail: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "loginEmail", origin: "origin:login-email") - } - public static var loginApple: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "loginApple", origin: "origin:login-apple") - } - public static var login2FA: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "login2FA", origin: "origin:login-2fa") - } - public static var loginWebauthn: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "loginWebauthn", origin: "origin:login-webauthn") - } - public static var loginMagicLink: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "loginMagicLink", origin: "origin:login-magic-link") - } - public static var loginSiteAddress: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "loginSiteAddress", origin: "origin:login-site-address") - } - - /// For `VerifyEmailViewController` - public static var verifyEmailInstructions: WordPressSupportSourceTag { - WordPressSupportSourceTag(name: "verifyEmailInstructions", origin: "origin:login-site-address") - } - - public static var loginUsernamePassword: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "loginUsernamePassword", origin: "origin:login-username-password") - } - public static var loginWPComUsernamePassword: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "loginWPComUsernamePassword", origin: "origin:wpcom-login-username-password") - } - public static var loginWPComPassword: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "loginWPComPassword", origin: "origin:login-wpcom-password") - } - public static var wpComSignupEmail: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "wpComSignupEmail", origin: "origin:wpcom-signup-email-entry") - } - public static var wpComSignup: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "wpComSignup", origin: "origin:signup-screen") - } - public static var wpComSignupWaitingForGoogle: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "wpComSignupWaitingForGoogle", origin: "origin:signup-waiting-for-google") - } - public static var wpComAuthWaitingForGoogle: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "wpComAuthWaitingForGoogle", origin: "origin:auth-waiting-for-google") - } - public static var wpComAuthGoogleSignupConfirmation: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "wpComAuthGoogleSignupConfirmation", origin: "origin:auth-google-signup-confirmation") - } - public static var wpComSignupMagicLink: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "wpComSignupMagicLink", origin: "origin:signup-magic-link") - } - public static var wpComSignupApple: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "wpComSignupApple", origin: "origin:signup-apple") - } - - public static var wpComLoginMagicLinkAutoRequested: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "wpComLoginMagicLinkAutoRequested", origin: "origin:login-email") - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Credentials/AuthenticatorCredentials.swift b/Sources/WordPressAuthenticator/Helpers/Credentials/AuthenticatorCredentials.swift deleted file mode 100644 index 0a9963e6118a..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Credentials/AuthenticatorCredentials.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -// MARK: - Authenticator Credentials -// -public struct AuthenticatorCredentials { - /// WordPress.com credentials - /// - public let wpcom: WordPressComCredentials? - - /// Self-hosted site credentials - /// - public let wporg: WordPressOrgCredentials? - - /// Designated initializer - /// - public init(wpcom: WordPressComCredentials? = nil, wporg: WordPressOrgCredentials? = nil) { - self.wpcom = wpcom - self.wporg = wporg - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Credentials/WordPressComCredentials.swift b/Sources/WordPressAuthenticator/Helpers/Credentials/WordPressComCredentials.swift deleted file mode 100644 index 205a42191344..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Credentials/WordPressComCredentials.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation - -// MARK: - WordPress.com Credentials -// -public struct WordPressComCredentials: Equatable { - - /// WordPress.com authentication token - /// - public let authToken: String - - /// Is this a Jetpack-connected site? - /// - public let isJetpackLogin: Bool - - /// Is 2-factor Authentication Enabled? - /// - public let multifactor: Bool - - /// The site address used during login - /// - public var siteURL: String - - private let wpComURL = "https://wordpress.com" - - /// Legacy initializer, for backwards compatibility - /// - public init(authToken: String, - isJetpackLogin: Bool, - multifactor: Bool, - siteURL: String = "https://wordpress.com") { - self.authToken = authToken - self.isJetpackLogin = isJetpackLogin - self.multifactor = multifactor - self.siteURL = !siteURL.isEmpty ? siteURL : wpComURL - } -} - -// MARK: - Equatable Conformance -// -public func ==(lhs: WordPressComCredentials, rhs: WordPressComCredentials) -> Bool { - return lhs.authToken == rhs.authToken && lhs.siteURL == rhs.siteURL -} diff --git a/Sources/WordPressAuthenticator/Helpers/Credentials/WordPressOrgCredentials.swift b/Sources/WordPressAuthenticator/Helpers/Credentials/WordPressOrgCredentials.swift deleted file mode 100644 index 6d951c14cd7a..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Credentials/WordPressOrgCredentials.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation - -// MARK: - WordPress.org (aka self-hosted site) Credentials -// -public struct WordPressOrgCredentials: Equatable { - /// Self-hosted login username. - /// The one used in the /wp-admin/ panel. - /// - public let username: String - - /// Self-hosted login password. - /// The one used in the /wp-admin/ panel. - /// - public let password: String - - /// The URL to reach the XMLRPC file. - /// e.g.: https://exmaple.com/xmlrpc.php - /// - public let xmlrpc: String - - /// Self-hosted site options - /// - public let options: [AnyHashable: Any] - - /// Designated initializer - /// - public init(username: String, password: String, xmlrpc: String, options: [AnyHashable: Any]) { - self.username = username - self.password = password - self.xmlrpc = xmlrpc - self.options = options - } - - /// Returns site URL by stripping "/xmlrpc.php" from `xmlrpc` String property - /// - public var siteURL: String { - xmlrpc.removingSuffix("/xmlrpc.php") - } -} - -// MARK: - Equatable Conformance -// -public func ==(lhs: WordPressOrgCredentials, rhs: WordPressOrgCredentials) -> Bool { - return lhs.username == rhs.username && lhs.password == rhs.password && lhs.xmlrpc == rhs.xmlrpc -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/ASWebAuthenticationSession+Utils.swift .swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/ASWebAuthenticationSession+Utils.swift .swift deleted file mode 100644 index 2d4891a5848c..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/ASWebAuthenticationSession+Utils.swift .swift +++ /dev/null @@ -1,20 +0,0 @@ -import AuthenticationServices - -extension ASWebAuthenticationSession { - - /// Wrapper around the default `init(url:, callbackULRScheme:, completionHandler:)` where the - /// `completionHandler` argument is a `Result` instead of a `URL` and `Error` pair. - convenience init(url: URL, callbackURLScheme: String, completionHandler: @escaping (Result) -> Void) { - self.init(url: url, callbackURLScheme: callbackURLScheme) { callbackURL, error in - completionHandler( - Result( - value: callbackURL, - error: error, - // Unfortunately we cannot exted `ASWebAuthenticationSessionError.Code` to add - // a custom error for this scenario, so we're left to use a "generic" one. - inconsistentStateError: OAuthError.inconsistentWebAuthenticationSessionCompletion - ) - ) - } - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Character+URLSafe.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Character+URLSafe.swift deleted file mode 100644 index 63d7bccc5264..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Character+URLSafe.swift +++ /dev/null @@ -1,10 +0,0 @@ -extension Character { - - // From the docs: using the unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" - // That is, URL safe characters. - // - // Notice that Swift offers `CharacterSet.urlQueryAllowed` to represent this set of characters. - // However, there is no straightforward way to convert a `CharacterSet` to a `Set`. - // See for example https://nshipster.com/characterset/. - static let urlSafeCharacters = Set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~") -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Data+Base64URL.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Data+Base64URL.swift deleted file mode 100644 index 30ed99509535..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Data+Base64URL.swift +++ /dev/null @@ -1,34 +0,0 @@ -public extension Data { - - /// "base64url" is an encoding that is safe to use with URLs. - /// It is defined in RFC 4648, section 5. - /// - /// See: - /// - https://tools.ietf.org/html/rfc4648#section-5 - /// - https://tools.ietf.org/html/rfc7515#appendix-C - init?(base64URLEncoded: String) { - let base64 = base64URLEncoded - .replacingOccurrences(of: "-", with: "+") - .replacingOccurrences(of: "_", with: "/") - - let length = Double(base64.lengthOfBytes(using: String.Encoding.utf8)) - let requiredLength = 4 * ceil(length / 4.0) - let paddingLength = requiredLength - length - if paddingLength > 0 { - let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0) - self.init(base64Encoded: base64 + padding, options: .ignoreUnknownCharacters) - } else { - self.init(base64Encoded: base64, options: .ignoreUnknownCharacters) - } - } - - /// See https://tools.ietf.org/html/rfc4648#section-5 - /// - /// Function name to match the standard library's `base64EncodedString()`. - func base64URLEncodedString() -> String { - base64EncodedString() - .replacingOccurrences(of: "/", with: "_") - .replacingOccurrences(of: "+", with: "-") - .replacingOccurrences(of: "=", with: "") - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Data+SHA256.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Data+SHA256.swift deleted file mode 100644 index 82f14056f7ba..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Data+SHA256.swift +++ /dev/null @@ -1,12 +0,0 @@ -import CryptoKit - -extension Data { - - func sha256Hashed() -> Data { - Data(SHA256.hash(data: self)) - } - - func sha256Hashed() -> String { - SHA256.hash(data: self).map { String(format: "%02hhx", $0) }.joined() - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/DataGetting.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/DataGetting.swift deleted file mode 100644 index f817743a93f9..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/DataGetting.swift +++ /dev/null @@ -1,4 +0,0 @@ -protocol DataGetting { - - func data(for request: URLRequest) async throws -> Data -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleClientId.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleClientId.swift deleted file mode 100644 index 4adb4cc29014..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleClientId.swift +++ /dev/null @@ -1,26 +0,0 @@ -public struct GoogleClientId { - - let value: String - - public init?(string: String) { - guard string.split(separator: ".").count > 1 else { - return nil - } - self.value = string - } - - /// See https://developers.google.com/identity/protocols/oauth2/native-app#step1-code-verifier - func redirectURI(path: String?) -> String { - let root = value.split(separator: ".").reversed().joined(separator: ".") - - guard let path else { - return root - } - - return "\(root):/\(path)" - } - - var defaultRedirectURI: String { - redirectURI(path: "oauth2callback") - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleOAuthTokenGetter.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleOAuthTokenGetter.swift deleted file mode 100644 index abfd0210c0f9..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleOAuthTokenGetter.swift +++ /dev/null @@ -1,28 +0,0 @@ -class GoogleOAuthTokenGetter: GoogleOAuthTokenGetting { - - let dataGetter: DataGetting - - init(dataGetter: DataGetting = URLSession.shared) { - self.dataGetter = dataGetter - } - - func getToken( - clientId: GoogleClientId, - audience: String, - authCode: String, - pkce: ProofKeyForCodeExchange - ) async throws -> OAuthTokenResponseBody { - let request = try URLRequest.googleSignInTokenRequest( - body: .googleSignInRequestBody( - clientId: clientId, - audience: audience, - authCode: authCode, - pkce: pkce - ) - ) - - let data = try await dataGetter.data(for: request) - - return try JSONDecoder().decode(OAuthTokenResponseBody.self, from: data) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleOAuthTokenGetting.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleOAuthTokenGetting.swift deleted file mode 100644 index c505494a3533..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleOAuthTokenGetting.swift +++ /dev/null @@ -1,9 +0,0 @@ -protocol GoogleOAuthTokenGetting { - - func getToken( - clientId: GoogleClientId, - audience: String, - authCode: String, - pkce: ProofKeyForCodeExchange - ) async throws -> OAuthTokenResponseBody -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/IDToken.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/IDToken.swift deleted file mode 100644 index 706a9015e9f6..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/IDToken.swift +++ /dev/null @@ -1,23 +0,0 @@ -/// See https://developers.google.com/identity/openid-connect/openid-connect#obtainuserinfo -public struct IDToken { - - public let token: JSONWebToken - public let name: String - public let email: String - - // TODO: Validate token! – https://developers.google.com/identity/openid-connect/openid-connect#validatinganidtoken - init?(jwt: JSONWebToken) { - // Name and email might not be part of the JWT Google sent us if the scope used for the - // request didn't include them - guard let email = jwt.payload["email"] as? String else { - return nil - } - guard let name = jwt.payload["name"] as? String else { - return nil - } - - self.token = jwt - self.name = name - self.email = email - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/JSONWebToken.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/JSONWebToken.swift deleted file mode 100644 index a45156756430..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/JSONWebToken.swift +++ /dev/null @@ -1,47 +0,0 @@ -/// Represents a JSON Web Token (JWT) -/// -/// See https://jwt.io/introduction -public struct JSONWebToken { - let rawValue: String - - let header: [String: Any] - let payload: [String: Any] - let signature: String - - init?(encodedString: String) { - let segments = encodedString.components(separatedBy: ".") - - // JWT has three segments: header, payload, and signature - guard segments.count == 3 else { - return nil - } - - // Notice that JWT uses base64url encoding, not base64. - // - // See: - // - https://tools.ietf.org/html/rfc7515#appendix-C - // - https://jwt.io/introduction - - // Note: Splitting the guards is useful to know which one fails - guard let headerData = Data(base64URLEncoded: segments[0]) else { - return nil - } - - guard let payloadData = Data(base64URLEncoded: segments[1]) else { - return nil - } - - guard let header = try? JSONSerialization.jsonObject(with: headerData, options: []) as? [String: Any] else { - return nil - } - - guard let payload = try? JSONSerialization.jsonObject(with: payloadData, options: []) as? [String: Any] else { - return nil - } - - self.rawValue = encodedString - self.header = header - self.payload = payload - self.signature = segments[2] - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/NewGoogleAuthenticator.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/NewGoogleAuthenticator.swift deleted file mode 100644 index f6b728d57d64..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/NewGoogleAuthenticator.swift +++ /dev/null @@ -1,123 +0,0 @@ -@preconcurrency import AuthenticationServices - -public class NewGoogleAuthenticator: NSObject { - - let clientId: GoogleClientId - let scheme: String - let audience: String - let oauthTokenGetter: GoogleOAuthTokenGetting - - public convenience init( - clientId: GoogleClientId, - scheme: String, - audience: String, - urlSession: URLSession - ) { - self.init( - clientId: clientId, - scheme: scheme, - audience: audience, - oautTokenGetter: GoogleOAuthTokenGetter(dataGetter: urlSession) - ) - } - - init( - clientId: GoogleClientId, - scheme: String, - audience: String, - oautTokenGetter: GoogleOAuthTokenGetting - ) { - self.clientId = clientId - self.scheme = scheme - self.audience = audience - self.oauthTokenGetter = oautTokenGetter - } - - /// Get the user's OAuth token from their Google account. This token can be used to authenticate with the WordPress backend. - /// - /// The app will present the browser to hand over authentication to Google from the given `UIViewController`. - public func getOAuthToken(from viewController: UIViewController) async throws -> IDToken { - return try await getOAuthToken( - from: WebAuthenticationPresentationContext(viewController: viewController) - ) - } - - /// Get the user's OAuth token from their Google account. This token can be used to authenticate with the WordPress backend. - /// - /// The app will present the browser to hand over authentication to Google using the given - /// `ASWebAuthenticationPresentationContextProviding`. - public func getOAuthToken( - from contextProvider: ASWebAuthenticationPresentationContextProviding - ) async throws -> IDToken { - let pkce = try ProofKeyForCodeExchange() - let url = try await getURL( - clientId: clientId, - scheme: scheme, - pkce: pkce, - contextProvider: contextProvider - ) - return try await requestOAuthToken(url: url, clientId: clientId, audience: audience, pkce: pkce) - } - - func getURL( - clientId: GoogleClientId, - scheme: String, - pkce: ProofKeyForCodeExchange, - contextProvider: ASWebAuthenticationPresentationContextProviding - ) async throws -> URL { - let url = try URL.googleSignInAuthURL(clientId: clientId, pkce: pkce) - return try await withCheckedThrowingContinuation { continuation in - let session = ASWebAuthenticationSession( - url: url, - callbackURLScheme: scheme, - completionHandler: { result in - continuation.resume(with: result) - } - ) - - session.presentationContextProvider = contextProvider - // At this point in time, we don't see the need to make the session ephemeral. - // - // Additionally, from a user's perspective, it would be frustrating to have to - // authenticate with Google again unless necessary—it certainly would be when testing - // the app. - session.prefersEphemeralWebBrowserSession = false - - // It feels inappropriate to force a dispatch on the main queue deep within the library. - // However, this is required to ensure `session` accesses the view it needs for the presentation on the right thread. - // - // See tradeoffs consideration at: - // https://github.com/wordpress-mobile/WordPressAuthenticator-iOS/pull/743#discussion_r1109325159 - DispatchQueue.main.async { - session.start() - } - } - } - - func requestOAuthToken( - url: URL, - clientId: GoogleClientId, - audience: String, - pkce: ProofKeyForCodeExchange - ) async throws -> IDToken { - guard let authCode = URLComponents(url: url, resolvingAgainstBaseURL: false)? - .queryItems? - .first(where: { $0.name == "code" })? - .value else { - throw OAuthError.urlDidNotContainCodeParameter(url: url) - } - - let response = try await oauthTokenGetter.getToken( - clientId: clientId, - audience: audience, - authCode: authCode, - pkce: pkce - ) - - guard let idToken = response.idToken else { - throw OAuthError.tokenResponseDidNotIncludeIdToken - } - - return idToken - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthError.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthError.swift deleted file mode 100644 index 329d3c3dd19d..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthError.swift +++ /dev/null @@ -1,24 +0,0 @@ -public enum OAuthError: LocalizedError { - - // ASWebAuthenticationSession - case inconsistentWebAuthenticationSessionCompletion - - case failedToGenerateSecureRandomCodeVerifier(status: Int32) - - // OAuth token response - case urlDidNotContainCodeParameter(url: URL) - case tokenResponseDidNotIncludeIdToken - - public var errorDescription: String { - switch self { - case .inconsistentWebAuthenticationSessionCompletion: - return "ASWebAuthenticationSession authentication finished with neither a callback URL nor error" - case .failedToGenerateSecureRandomCodeVerifier(let status): - return "Could not generate a cryptographically secure random PKCE code verifier value. Underlying error code \(status)" - case .urlDidNotContainCodeParameter(let url): - return "Could not find 'code' parameter in URL '\(url)'" - case .tokenResponseDidNotIncludeIdToken: - return "OAuth token response did not include idToken" - } - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthRequestBody+GoogleSignIn.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthRequestBody+GoogleSignIn.swift deleted file mode 100644 index 7f3df6b157b5..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthRequestBody+GoogleSignIn.swift +++ /dev/null @@ -1,26 +0,0 @@ -extension OAuthTokenRequestBody { - - static func googleSignInRequestBody( - clientId: GoogleClientId, - audience: String, - authCode: String, - pkce: ProofKeyForCodeExchange - ) -> Self { - .init( - clientId: clientId.value, - // "The client secret obtained from the API Console Credentials page." - // - https://developers.google.com/identity/protocols/oauth2/native-app#step-2:-send-a-request-to-googles-oauth-2.0-server - // - // There doesn't seem to be any secret for iOS app credentials. - // The process works with an empty string... - clientSecret: "", - audience: audience, - code: authCode, - codeVerifier: pkce.codeVerifier, - // As defined in the OAuth 2.0 specification, this field's value must be set to authorization_code. - // – https://developers.google.com/identity/protocols/oauth2/native-app#exchange-authorization-code - grantType: "authorization_code", - redirectURI: clientId.defaultRedirectURI - ) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthTokenRequestBody.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthTokenRequestBody.swift deleted file mode 100644 index 49cd824b4009..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthTokenRequestBody.swift +++ /dev/null @@ -1,45 +0,0 @@ -/// Models the request to send for an OAuth token -/// -/// - Note: See documentation at https://developers.google.com/identity/protocols/oauth2/native-app#exchange-authorization-code -struct OAuthTokenRequestBody { - let clientId: String - let clientSecret: String - let audience: String - let code: String - let codeVerifier: ProofKeyForCodeExchange.CodeVerifier - let grantType: String - let redirectURI: String - - enum CodingKeys: String, CodingKey { - case clientId = "client_id" - case clientSecret = "client_secret" - case audience - case code - case codeVerifier = "code_verifier" - case grantType = "grant_type" - case redirectURI = "redirect_uri" - } - - func asURLEncodedData() throws -> Data { - let params = [ - (CodingKeys.clientId.rawValue, clientId), - (CodingKeys.clientSecret.rawValue, clientSecret), - (CodingKeys.code.rawValue, code), - (CodingKeys.codeVerifier.rawValue, codeVerifier.rawValue), - (CodingKeys.grantType.rawValue, grantType), - (CodingKeys.redirectURI.rawValue, redirectURI), - // This is not in the spec at - // https://developers.google.com/identity/protocols/oauth2/native-app#step-2:-send-a-request-to-googles-oauth-2.0-server - // but we'll get an idToken that our backend considers invalid if omitted. - (CodingKeys.audience.rawValue, audience), - ] - - let items = params.map { URLQueryItem(name: $0.0, value: $0.1) } - - var components = URLComponents() - components.queryItems = items - - // We can assume `query` to never be nil because we set `queryItems` in the line above. - return Data(components.query!.utf8) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthTokenResponseBody.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthTokenResponseBody.swift deleted file mode 100644 index 145df1691d72..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthTokenResponseBody.swift +++ /dev/null @@ -1,35 +0,0 @@ -/// Models the response to an OAuth token request. -/// -/// - Note: See documentation at https://developers.google.com/identity/protocols/oauth2/native-app#exchange-authorization-code -struct OAuthTokenResponseBody: Codable, Equatable { - let accessToken: String - let expiresIn: Int - /// This value is only returned if the request included an identity scope, such as openid, profile, or email. - /// The value is a JSON Web Token (JWT) that contains digitally signed identity information about the user. - let rawIDToken: String? - let refreshToken: String? - let scope: String - /// The type of token returned. At this time, this field's value is always set to Bearer. - let tokenType: String - - var idToken: IDToken? { - guard let rawIDToken else { - return nil - } - - guard let jwt = JSONWebToken(encodedString: rawIDToken) else { - return nil - } - - return IDToken(jwt: jwt) - } - - enum CodingKeys: String, CodingKey { - case accessToken = "access_token" - case expiresIn = "expires_in" - case rawIDToken = "id_token" - case refreshToken = "refresh_token" - case scope - case tokenType = "token_type" - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/ProofKeyForCodeExchange.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/ProofKeyForCodeExchange.swift deleted file mode 100644 index 84d8f6ad2818..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/ProofKeyForCodeExchange.swift +++ /dev/null @@ -1,120 +0,0 @@ -// See: -// - https://developers.google.com/identity/protocols/oauth2/native-app#step1-code-verifier -// - https://www.rfc-editor.org/rfc/rfc7636 -// -// Note: The common abbreviation of "Proof Key for Code Exchange" is PKCE and is pronounced "pixy". -struct ProofKeyForCodeExchange: Equatable { - - enum Method: Equatable { - case s256 - case plain - - var urlQueryParameterValue: String { - switch self { - case .plain: return "plain" - case .s256: return "S256" - } - } - } - - let codeVerifier: CodeVerifier - let method: Method - - init() throws { - self.codeVerifier = try .makeRandomCodeVerifier() - self.method = .s256 - } - - init(codeVerifier: CodeVerifier, method: Method) { - self.codeVerifier = codeVerifier - self.method = method - } - - var codeChallenge: String { - codeVerifier.codeChallenge(using: method) - } -} - -extension ProofKeyForCodeExchange { - - // A code_verifier is a high-entropy cryptographic random string using the unreserved - // characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~", with a minimum length of 43 - // characters and a maximum length of 128 characters. - // - // The code verifier should have enough entropy to make it impractical to guess the value. - // - // See: - // - https://www.rfc-editor.org/rfc/rfc7636#section-4.1 - // - https://developers.google.com/identity/protocols/oauth2/native-app#step1-code-verifier - struct CodeVerifier: Equatable { - - let rawValue: String - - static let allowedCharacters = Character.urlSafeCharacters - static let allowedLengthRange = (43...128) - - /// Generates a random code verifier according to the PKCE RFC. - /// - /// - Note: This method name is more verbose than the recommended "make" for this factory to communicate the randomness component. - static func makeRandomCodeVerifier() throws -> Self { - let value = try randomSecureCodeVerifier() - - // It's appropriate to force unwrap here because a `nil` value could only result from - // a developer error—either wrong coding of the constrained length or of the allowed - // characters. - return .init(value: value)! - } - - init?(value: String) { - guard CodeVerifier.allowedLengthRange.contains(value.count) else { return nil } - - guard Set(value).isSubset(of: CodeVerifier.allowedCharacters) else { return nil } - - self.rawValue = value - } - - func codeChallenge(using method: Method) -> String { - switch method { - case .s256: - // The spec defines code_challenge for the s256 mode as: - // - // code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) - // - // We don't need the ASCII conversion, because we build `CodeVerifier` from URL safe - // characters. - let rawData = Data(rawValue.utf8) - let hashedData: Data = rawData.sha256Hashed() - return hashedData.base64URLEncodedString() - case .plain: - return rawValue - } - } - } - - /// Generates a random code verifier according to the PKCE RFC. - /// - /// The RFC states: - /// - /// > It is RECOMMENDED that the output of a suitable random number generator be used to create a 32-octet sequence. - /// > The octet sequence is then base64url-encoded to produce a 43-octet URL safe string to use as the code verifier. - static func randomSecureCodeVerifier() throws -> String { - let byteCount = 32 - var bytes = [UInt8](repeating: 0, count: byteCount) - let result = SecRandomCopyBytes(kSecRandomDefault, byteCount, &bytes) - - guard result == errSecSuccess else { - throw OAuthError.failedToGenerateSecureRandomCodeVerifier(status: result) - } - - let data = Data(bytes) - - // Base64url-encoding a 32-octect sequence should always result in a 43-length string, - // string, but let's cap it just in case. - // - // Also notice that by base64url-encoding, we ensure the characters are in the allowed - // set. - // - // 43 is also the minimum length for a code verifier, hence the `allowedLengthRange.lowerBound` usage. - return String(data.base64URLEncodedString().prefix(CodeVerifier.allowedLengthRange.lowerBound)) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Result+ConvenienceInit.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Result+ConvenienceInit.swift deleted file mode 100644 index 2ea91504226a..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Result+ConvenienceInit.swift +++ /dev/null @@ -1,15 +0,0 @@ -extension Swift.Result { - - /// Convenience init to lift the values in an Objective-C style callback, where both success and failure parameters can be nil, to - /// a domain where at least one is not nil. - /// - /// If both values are nil, it will create a `failure` instance wrapping the given `inconsistentStateError`. - init(value: Success?, error: Failure?, inconsistentStateError: Failure) { - switch (value, error) { - case (.some(let value), .none): self = .success(value) - case (.some, .some(let error)): self = .failure(error) - case (.none, .some(let error)): self = .failure(error) - case (.none, .none): self = .failure(inconsistentStateError) - } - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URL+GoogleSignIn.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URL+GoogleSignIn.swift deleted file mode 100644 index 3aea2b9418bd..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URL+GoogleSignIn.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation - -// It's acceptable to force-unwrap here because, for this call to fail we'd need a developer error, -// which we would catch because the unit tests would crash. -extension URL { - - static var googleSignInBaseURL = URL(string: "https://accounts.google.com/o/oauth2/v2/auth")! - - static var googleSignInOAuthTokenURL = URL(string: "https://oauth2.googleapis.com/token")! -} - -extension URL { - - static func googleSignInAuthURL(clientId: GoogleClientId, pkce: ProofKeyForCodeExchange) throws -> URL { - let queryItems = [ - ("client_id", clientId.value), - ("code_challenge", pkce.codeChallenge), - ("code_challenge_method", pkce.method.urlQueryParameterValue), - ("redirect_uri", clientId.defaultRedirectURI), - ("response_type", "code"), - // See what the Google SDK does: - // https://github.com/google/GoogleSignIn-iOS/blob/7.0.0/GoogleSignIn/Sources/GIDScopes.m#L58-L61 - ("scope", "profile email") - ].map { URLQueryItem(name: $0.0, value: $0.1) } - - return googleSignInBaseURL.appending(queryItems: queryItems) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URLRequest+GoogleSignIn.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URLRequest+GoogleSignIn.swift deleted file mode 100644 index 5bd8c78275e1..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URLRequest+GoogleSignIn.swift +++ /dev/null @@ -1,18 +0,0 @@ -extension URLRequest { - - static func googleSignInTokenRequest( - body: OAuthTokenRequestBody - ) throws -> URLRequest { - var request = URLRequest(url: URL.googleSignInOAuthTokenURL) - request.httpMethod = "POST" - - request.setValue( - "application/x-www-form-urlencoded; charset=UTF-8", - forHTTPHeaderField: "Content-Type" - ) - - request.httpBody = try body.asURLEncodedData() - - return request - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URLSesison+DataGetting.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URLSesison+DataGetting.swift deleted file mode 100644 index fef3acbc528a..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URLSesison+DataGetting.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -// In other projects, we avoid extending `URLSession` to conform to "-getting" protocols shaped -// like `DataGetting` and prefer composition instead, creating an object conforming to the protocol -// and holding an `URLSession` reference. -// -// The concern with extending a Foundation type with special-purpose domain object getting ability -// is that it would pollute the namespace, offering the protocol methods as an option everywhere -// `URLSession` is used, even in part of the app that are unrelated with the resource. -// -// But since the type `DataGetting` revolves around is `Data` and `URLSession` already exposes -// methods returning `Data`, this feels more like a syntax-sugar extension rather than one adding -// whole new domain-specific APIs to the type. -extension URLSession: DataGetting { - - func data(for request: URLRequest) async throws -> Data { - let (data, _) = try await data(for: request) - return data - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/LoginFacade.h b/Sources/WordPressAuthenticator/Helpers/LoginFacade.h deleted file mode 100644 index 01a7aa40206f..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/LoginFacade.h +++ /dev/null @@ -1,176 +0,0 @@ -#import - -// This is a strange hack to make the `-[LoginFacadeDelegate needsMultifactorCodeForUserID:andNonceInfo:]` -// available in Objective-C runtime. -@import WordPressKit; - -NS_ASSUME_NONNULL_BEGIN - -@class LoginFields; -@class SocialLogin2FANonceInfo; -@protocol WordPressComOAuthClientFacadeProtocol; -@protocol WordPressXMLRPCAPIFacade; -@protocol LoginFacadeDelegate; - - -/** - * This protocol represents a class that handles the signing in to a self hosted/.com site. - */ -@protocol LoginFacade - -/** - * This method initializes the LoginFacade instance. - * - * @param dotcomClientID WordPress.com Client ID. - * @param dotcomSecret WordPress.com Secret. - * @param userAgent UserAgent string to be used interacting with a remote endpoint. - * - * @note When this class is Swifted, we can (probably) just query WordPressAuthenticator. - */ -- (instancetype)initWithDotcomClientID:(NSString *)dotcomClientID dotcomSecret:(NSString *)dotcomSecret userAgent:(NSString *)userAgent; - -/** - * This method will attempt to sign in to a self hosted/.com site. - * XMLRPC endpoint discover is performed. - * - * @param loginFields the fields representing the site we are attempting to login to. - */ -- (void)signInWithLoginFields:(LoginFields *)loginFields; - -/** - * This method will attempt to sign in to a self hosted/.com site. - * The XML-RPC endpoint should be present in the loginFields.siteUrl field. - * - * @param loginFields the fields representing the site we are attempting to login to. - */ -- (void)loginToSelfHosted:(LoginFields *)loginFields; - -/** - * Social login. - * - * @param token Social id token. - */ -- (void)loginToWordPressDotComWithSocialIDToken:(NSString *)token - service:(NSString *)service; - -/** - * Social login via a social account with 2FA using a nonce. - * - * @param userID WordPress.com User ID - * @param authType Indicates the Kind of Authentication we're performing (sms / authenticator / etc). - * @param twoStepCode Multifactor Code. - * @param twoStepNonce Two Step Nonce. - */ -- (void)loginToWordPressDotComWithUser:(NSInteger)userID - authType:(NSString *)authType - twoStepCode:(NSString *)twoStepCode - twoStepNonce:(NSString *)twoStepNonce; - - -/** - * A delegate with a few methods that indicate various aspects of the login process - */ -@property (nonatomic, weak) id delegate; - -/** - * A class that handles the login to sites requiring oauth(primarily .com sites) - */ -@property (nonatomic, strong) id wordpressComOAuthClientFacade; - -/** - * A class that handles the login to self hosted sites - */ -@property (nonatomic, strong) id wordpressXMLRPCAPIFacade; - -@end - -/** - * This class handles the signing in to a self hosted/.com site. - */ -@interface LoginFacade : NSObject - -@end - -/** - * Protocol with a few methods that indicate various aspects of the login process. - */ -@protocol LoginFacadeDelegate - -@optional - -/** - * This is called when we need to indicate to the a messagea about the current login (e.g. "Signing In", "Authenticating", "Syncing", etc.) - * - * @param message the message to display to the user. - */ -- (void)displayLoginMessage:(NSString *)message; - -/** - * This is called when the initial login failed because we need a 2fa code. - */ -- (void)needsMultifactorCode; - -/** - * This is called when the initial login failed because we need a 2fa code for a social login. - * - * @param userID the WPCom userID of the user logging in. - * @param nonceInfo an object containing information about available 2fa nonce options. - */ -- (void)needsMultifactorCodeForUserID:(NSInteger)userID andNonceInfo:(SocialLogin2FANonceInfo *)nonceInfo; - -/** - * This is called when there's been an error and we want to inform the user. - * - * @param error the error in question. - */ -- (void)displayRemoteError:(NSError *)error; - -/** - * Called when finished logging into a self hosted site - * - * @param username username of the site - * @param password password of the site - * @param xmlrpc the xmlrpc url of the site - * @param options the options dictionary coming back from the `wp.getOptions` method. - */ -- (void)finishedLoginWithUsername:(NSString *)username password:(NSString *)password xmlrpc:(NSString *)xmlrpc options:(NSDictionary * )options; - - -/** - * Called when finished logging in to a WordPress.com site - * - * @param authToken authToken to be used to access the site - * @param requiredMultifactorCode whether the login required a 2fa code - */ -- (void)finishedLoginWithAuthToken:(NSString *)authToken requiredMultifactorCode:(BOOL)requiredMultifactorCode; - - -/** - * Called when finished logging in to a WordPress.com site via a Google token. - * - * @param googleIDToken the token used - * @param authToken authToken to be used to access the site - */ -- (void)finishedLoginWithGoogleIDToken:(NSString *)googleIDToken authToken:(NSString *)authToken; - - -/** - * Called when finished logging in to a WordPress.com site via a 2FA Nonce. - * - * @param authToken authToken to be used to access the site - */ -- (void)finishedLoginWithNonceAuthToken:(NSString *)authToken; - - -/** - * Lets the delegate know that a social login attempt found a matching user, but - * their account has not been connected to the social service previously. - * - * @param email The email address that was matched. - */ -- (void)existingUserNeedsConnection:(NSString *)email; - -@end - -NS_ASSUME_NONNULL_END - diff --git a/Sources/WordPressAuthenticator/Helpers/LoginFacade.m b/Sources/WordPressAuthenticator/Helpers/LoginFacade.m deleted file mode 100644 index 333865b345d6..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/LoginFacade.m +++ /dev/null @@ -1,179 +0,0 @@ -#import "LoginFacade.h" -#import "WordPressXMLRPCAPIFacade.h" -#import -@import NSURL_IDN; -@import WordPressKit; - -@implementation LoginFacade - -@synthesize delegate; -@synthesize wordpressComOAuthClientFacade = _wordpressComOAuthClientFacade; -@synthesize wordpressXMLRPCAPIFacade = _wordpressXMLRPCAPIFacade; - -- (instancetype)initWithDotcomClientID:(NSString *)dotcomClientID dotcomSecret:(NSString *)dotcomSecret userAgent:(NSString *)userAgent -{ - self = [super init]; - if (self) { - _wordpressComOAuthClientFacade = [[WordPressComOAuthClientFacade alloc] initWithClient:dotcomClientID secret:dotcomSecret]; - _wordpressXMLRPCAPIFacade = [[WordPressXMLRPCAPIFacade alloc] initWithUserAgent:userAgent]; - } - return self; -} - -- (void)signInWithLoginFields:(LoginFields *)loginFields -{ - NSAssert(self.delegate != nil, @"Must set delegate to use service"); - - if (loginFields.userIsDotCom || loginFields.siteAddress.isWordPressComPath) { - [self signInToWordpressDotCom:loginFields]; - } else { - [self signInToSelfHosted:loginFields]; - } -} - -- (void)loginToWordPressDotComWithSocialIDToken:(NSString *)token - service:(NSString *)service -{ - if ([self.delegate respondsToSelector:@selector(displayLoginMessage:)]) { - [self.delegate displayLoginMessage:NSLocalizedString(@"Connecting to WordPress.com", nil)]; - } - - [self.wordpressComOAuthClientFacade authenticateWithSocialIDToken:token - service:service - success:^(NSString *authToken) { - if ([service isEqualToString:@"google"] && [self.delegate respondsToSelector:@selector(finishedLoginWithGoogleIDToken:authToken:)]) { - // Apple is handled in AppleAuthenticator - [self.delegate finishedLoginWithGoogleIDToken:token authToken:authToken]; - } - [self trackSuccess]; - } needsMultifactor:^(NSInteger userID, SocialLogin2FANonceInfo *nonceInfo){ - if ([self.delegate respondsToSelector:@selector(needsMultifactorCodeForUserID:andNonceInfo:)]) { - [self.delegate needsMultifactorCodeForUserID:userID andNonceInfo:nonceInfo]; - } - } existingUserNeedsConnection: ^(NSString *email) { - // Apple is handled in AppleAuthenticator - if ([self.delegate respondsToSelector:@selector(existingUserNeedsConnection:)]) { - [self.delegate existingUserNeedsConnection: email]; - } - } failure:^(NSError *error) { - [self track:WPAnalyticsStatLoginFailed error:error]; - [self track:WPAnalyticsStatLoginSocialFailure error:error]; - if ([self.delegate respondsToSelector:@selector(displayRemoteError:)]) { - [self.delegate displayRemoteError:error]; - } - }]; -} - -- (void)loginToWordPressDotComWithUser:(NSInteger)userID - authType:(NSString *)authType - twoStepCode:(NSString *)twoStepCode - twoStepNonce:(NSString *)twoStepNonce -{ - if ([self.delegate respondsToSelector:@selector(displayLoginMessage:)]) { - [self.delegate displayLoginMessage:NSLocalizedString(@"Connecting to WordPress.com", nil)]; - } - - [self.wordpressComOAuthClientFacade authenticateWithSocialLoginUser:userID - authType:authType - twoStepCode:twoStepCode - twoStepNonce:twoStepNonce - success:^(NSString *authToken) { - if ([self.delegate respondsToSelector:@selector(finishedLoginWithNonceAuthToken:)]) { - [self.delegate finishedLoginWithNonceAuthToken:authToken]; - } - [self trackSuccess]; - } failure:^(NSError *error) { - [self track:WPAnalyticsStatLoginFailed error:error]; - if ([self.delegate respondsToSelector:@selector(displayRemoteError:)]) { - [self.delegate displayRemoteError:error]; - } - }]; -} - -- (void)signInToWordpressDotCom:(LoginFields *)loginFields -{ - if ([self.delegate respondsToSelector:@selector(displayLoginMessage:)]) { - [self.delegate displayLoginMessage:NSLocalizedString(@"Connecting to WordPress.com", nil)]; - } - - [self.wordpressComOAuthClientFacade authenticateWithUsername:loginFields.username password:loginFields.password multifactorCode:loginFields.multifactorCode success:^(NSString *authToken) { - if ([self.delegate respondsToSelector:@selector(finishedLoginWithAuthToken:requiredMultifactorCode:)]) { - [self.delegate finishedLoginWithAuthToken:authToken requiredMultifactorCode:loginFields.requiredMultifactor]; - } - [self trackSuccess]; - } needsMultifactor:^(NSInteger userID, SocialLogin2FANonceInfo *nonceInfo) { - if (nonceInfo == nil && [self.delegate respondsToSelector:@selector(needsMultifactorCode)]) { - [self.delegate needsMultifactorCode]; - } else if (nonceInfo != nil && [self.delegate respondsToSelector:@selector(needsMultifactorCodeForUserID:andNonceInfo:)]) { - [self.delegate needsMultifactorCodeForUserID:userID andNonceInfo:nonceInfo]; - } - } failure:^(NSError *error) { - [self track:WPAnalyticsStatLoginFailed error:error]; - if ([self.delegate respondsToSelector:@selector(displayRemoteError:)]) { - [self.delegate displayRemoteError:error]; - } - }]; -} - -- (void)signInToSelfHosted:(LoginFields *)loginFields -{ - void (^guessXMLRPCURLSuccess)(NSURL *) = ^(NSURL *xmlRPCURL) { - loginFields.xmlrpcURL = xmlRPCURL; - [self loginToSelfHosted:loginFields]; - }; - - void (^guessXMLRPCURLFailure)(NSError *) = ^(NSError *error){ - [self track:WPAnalyticsStatLoginFailedToGuessXMLRPC error:error]; - [self track:WPAnalyticsStatLoginFailed error:error]; - [self.delegate displayRemoteError:error]; - }; - - if ([self.delegate respondsToSelector:@selector(displayLoginMessage:)]) { - [self.delegate displayLoginMessage:NSLocalizedString(@"Authenticating", nil)]; - } - - NSString *siteUrl = [NSURL IDNEncodedURL: loginFields.siteAddress]; - [self.wordpressXMLRPCAPIFacade guessXMLRPCURLForSite:siteUrl success:guessXMLRPCURLSuccess failure:guessXMLRPCURLFailure]; -} - -- (void)loginToSelfHosted:(LoginFields *)loginFields -{ - NSURL *xmlRPCURL = loginFields.xmlrpcURL; - [self.wordpressXMLRPCAPIFacade getBlogOptionsWithEndpoint:xmlRPCURL username:loginFields.username password:loginFields.password success:^(id options) { - if ([options objectForKey:@"wordpress.com"] != nil) { - [self signInToWordpressDotCom:loginFields]; - } else { - NSString *versionString = options[@"software_version"][@"value"]; - NSString *minimumSupported = [WordPressOrgXMLRPCApi minimumSupportedVersion]; - CGFloat version = [versionString floatValue]; - - if (version > 0 && version < [minimumSupported floatValue]) { - NSString *errorMessage = [NSString stringWithFormat:NSLocalizedString(@"WordPress version too old. The site at %@ uses WordPress %@. We recommend to update to the latest version, or at least %@", nil), [xmlRPCURL host], versionString, minimumSupported]; - NSError *versionError = [NSError errorWithDomain:WordPressAuthenticator.errorDomain - code:WordPressAuthenticator.invalidVersionErrorCode - userInfo:@{NSLocalizedDescriptionKey:errorMessage}]; - [self track:WPAnalyticsStatLoginFailed error:versionError]; - [self.delegate displayRemoteError:versionError]; - return; - } - NSString *xmlrpc = [xmlRPCURL absoluteString]; - [self.delegate finishedLoginWithUsername:loginFields.username password:loginFields.password xmlrpc:xmlrpc options:options]; - [self trackSuccess]; - } - } failure:^(NSError *error) { - [self track:WPAnalyticsStatLoginFailed error:error]; - [self.delegate displayRemoteError:error]; - }]; -} - -- (void)track:(WPAnalyticsStat)stat -{ - [WordPressAuthenticator track:stat]; -} - -- (void)track:(WPAnalyticsStat)stat error:(NSError *)error -{ - [WordPressAuthenticator track:stat error:error]; -} - -@end diff --git a/Sources/WordPressAuthenticator/Helpers/LoginFacade.swift b/Sources/WordPressAuthenticator/Helpers/LoginFacade.swift deleted file mode 100644 index c2117e235cda..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/LoginFacade.swift +++ /dev/null @@ -1,111 +0,0 @@ -import Foundation -import WordPressKit -import WordPressShared - -/// Extension for handling 2FA authentication. -/// -public extension LoginFacade { - private var tracker: AuthenticatorAnalyticsTracker { - AuthenticatorAnalyticsTracker.shared - } - - func requestOneTimeCode(with loginFields: LoginFields) { - wordpressComOAuthClientFacade.requestOneTimeCode( - username: loginFields.username, - password: loginFields.password, - success: { [weak self] in - guard let self else { - return - } - - if self.tracker.shouldUseLegacyTracker() { - WordPressAuthenticator.track(.twoFactorSentSMS) - } - }) { _ in - WPLogError("Failed to request one time code") - } - } - - func requestSocial2FACode(with loginFields: LoginFields) { - guard let nonce = loginFields.nonceInfo?.nonceSMS else { - return - } - - wordpressComOAuthClientFacade.requestSocial2FACode( - userID: loginFields.nonceUserID, - nonce: nonce, - success: { [weak self] newNonce in - guard let self else { - return - } - - loginFields.nonceInfo?.nonceSMS = newNonce - - if self.tracker.shouldUseLegacyTracker() { - WordPressAuthenticator.track(.twoFactorSentSMS) - } - }) { _, newNonce in - if let newNonce { - loginFields.nonceInfo?.nonceSMS = newNonce - } - WPLogError("Failed to request one time code") - } - } - - /// Async function that returns the necessary `WebauthnChallengeInfo` to start allow for a security key log in. - /// - func requestWebauthnChallenge(userID: Int, twoStepNonce: String) async -> WebauthnChallengeInfo? { - - delegate?.displayLoginMessage?(NSLocalizedString("Waiting for security key", comment: "Text while waiting for a security key challenge")) - - return await withCheckedContinuation { continuation in - wordpressComOAuthClientFacade.requestWebauthnChallenge(userID: Int64(userID), twoStepNonce: twoStepNonce, success: { challengeInfo in - continuation.resume(returning: challengeInfo) - }, failure: { [weak self] error in - guard let self else { return } - - WPLogError("Failed to request webauthn challenge \(error)") - WordPressAuthenticator.track(.loginFailed, error: error) - continuation.resume(returning: nil) - - DispatchQueue.main.async { - self.delegate?.displayRemoteError?(error) - } - }) - } - } - - /// Forwards the authentication signature message and updates delegates accordingly. - /// - func authenticateWebauthnSignature(userID: Int, - twoStepNonce: String, - credentialID: Data, - clientDataJson: Data, - authenticatorData: Data, - signature: Data, - userHandle: Data) { - - delegate?.displayLoginMessage?(NSLocalizedString("Waiting for security key", comment: "Text while the webauthn signature is being verified")) - - wordpressComOAuthClientFacade.authenticateWebauthnSignature(userID: Int64(userID), - twoStepNonce: twoStepNonce, - credentialID: credentialID, - clientDataJson: clientDataJson, - authenticatorData: authenticatorData, - signature: signature, - userHandle: userHandle, - success: { [weak self] accessToken in - self?.delegate?.finishedLogin?(withNonceAuthToken: accessToken) - self?.trackSuccess() - }, failure: { [weak self] error in - WPLogError("Failed to verify webauthn signature \(error)") - WordPressAuthenticator.track(.loginFailed, error: error) - self?.delegate?.displayRemoteError?(error) - }) - } - - @objc - func trackSuccess() { - tracker.track(step: .success) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Model/LoginFields+Validation.swift b/Sources/WordPressAuthenticator/Helpers/Model/LoginFields+Validation.swift deleted file mode 100644 index a22cd6cb1692..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Model/LoginFields+Validation.swift +++ /dev/null @@ -1,47 +0,0 @@ -// MARK: - LoginFields Validation Methods -// -extension LoginFields { - - /// Returns *true* if the fields required for SignIn have been populated. - /// Note: that loginFields.emailAddress is not checked. Use loginFields.username instead. - /// - func validateFieldsPopulatedForSignin() -> Bool { - return !username.isEmpty && - !password.isEmpty && - (meta.userIsDotCom || !siteAddress.isEmpty) - } - - /// Returns *true* if the siteURL contains a valid URL. False otherwise. - /// - func validateSiteForSignin() -> Bool { - guard let url = URL(string: NSURL.idnEncodedURL(siteAddress)) else { - return false - } - - return !url.absoluteString.isEmpty - } - - /// Returns *true* if the credentials required for account creation have been provided. - /// - func validateFieldsPopulatedForCreateAccount() -> Bool { - return !emailAddress.isEmpty && - !username.isEmpty && - !password.isEmpty && - !siteAddress.isEmpty - } - - /// Returns *true* if no spaces have been used in [email, username, address] - /// - func validateFieldsForSigninContainNoSpaces() -> Bool { - let space = " " - return !emailAddress.contains(space) && - !username.contains(space) && - !siteAddress.contains(space) - } - - /// Returns *true* if the username is 50 characters or less. - /// - func validateUsernameMaxLength() -> Bool { - return username.count <= 50 - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Model/LoginFields.swift b/Sources/WordPressAuthenticator/Helpers/Model/LoginFields.swift deleted file mode 100644 index db3bbaf4e2b1..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Model/LoginFields.swift +++ /dev/null @@ -1,143 +0,0 @@ -import Foundation -import WordPressKit - -/// LoginFields is a state container for user textfield input on the login screens -/// as well as other meta data regarding the nature of a login attempt. -/// -@objc -public class LoginFields: NSObject { - // These fields store user input from text fields. - - /// Stores the user's account identifier (either email address or username) that is - /// entered in the login flow. By convention, even if the user is logging in - /// via an email address this field should store that value. - @objc public var username = "" - - /// The user's password. - @objc public var password = "" - - /// The site address if logging in via the self-hosted flow. - @objc public var siteAddress = "" - - /// The two factor code entered by a user. - @objc public var multifactorCode = "" // 2fa code - - /// Nonce info in the event of a social login with 2fa - @objc public var nonceInfo: SocialLogin2FANonceInfo? - - /// User ID for use with the nonce for social login - @objc public var nonceUserID: Int = 0 - - /// Used to restrict login to WordPress.com - public var restrictToWPCom = false - - /// Used on the webauthn/security key flow. - public var webauthnChallengeInfo: WebauthnChallengeInfo? - - /// Used by the SignupViewController. Signup currently asks for both a - /// username and an email address. This can be factored away when we revamp - /// the signup flow. - @objc public var emailAddress = "" - - var meta = LoginFieldsMeta() - - @objc public var userIsDotCom: Bool { - get { meta.userIsDotCom } - set { meta.userIsDotCom = newValue } - } - - @objc public var requiredMultifactor: Bool { - meta.requiredMultifactor - } - - @objc public var xmlrpcURL: NSURL? { - get { meta.xmlrpcURL } - set { meta.xmlrpcURL = newValue } - } - - var storedCredentials: SafariStoredCredentials? - - /// Convenience method for persisting stored credentials. - /// - @objc func setStoredCredentials(usernameHash: Int, passwordHash: Int) { - storedCredentials = SafariStoredCredentials() - storedCredentials?.storedUserameHash = usernameHash - storedCredentials?.storedPasswordHash = passwordHash - } - - class func makeForWPCom(username: String, password: String) -> LoginFields { - let loginFields = LoginFields() - - loginFields.username = username - loginFields.password = password - - return loginFields - } - - /// Using a convenience initializer for its Objective-C usage in unit tests. - convenience init(username: String, - password: String, - siteAddress: String, - multifactorCode: String, - nonceInfo: SocialLogin2FANonceInfo?, - nonceUserID: Int, - restrictToWPCom: Bool, - emailAddress: String, - meta: LoginFieldsMeta, - storedCredentials: SafariStoredCredentials?) { - self.init() - self.username = username - self.password = password - self.siteAddress = siteAddress - self.multifactorCode = multifactorCode - self.nonceInfo = nonceInfo - self.nonceUserID = nonceUserID - self.restrictToWPCom = restrictToWPCom - self.emailAddress = emailAddress - self.meta = meta - self.storedCredentials = storedCredentials - } -} - -extension LoginFields { - func copy() -> LoginFields { - .init(username: username, - password: password, - siteAddress: siteAddress, - multifactorCode: multifactorCode, - nonceInfo: nonceInfo, - nonceUserID: nonceUserID, - restrictToWPCom: restrictToWPCom, - emailAddress: emailAddress, - meta: meta.copy(), - storedCredentials: storedCredentials) - } -} - -extension LoginFields { - - var parametersForSignInWithApple: [String: AnyObject]? { - guard let user = meta.socialUser, case .apple = user.service else { - return nil - } - - return AccountServiceRemoteREST.appleSignInParameters( - email: user.email, - fullName: user.fullName - ) - } -} - -/// A helper class for storing safari saved password information. -/// -class SafariStoredCredentials { - var storedUserameHash = 0 - var storedPasswordHash = 0 -} - -/// An enum to indicate where the Magic Link Email was sent from. -/// -enum EmailMagicLinkSource: Int { - case login = 1 - case signup = 2 -} diff --git a/Sources/WordPressAuthenticator/Helpers/Model/LoginFieldsMeta.swift b/Sources/WordPressAuthenticator/Helpers/Model/LoginFieldsMeta.swift deleted file mode 100644 index 18dd5c49be5d..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Model/LoginFieldsMeta.swift +++ /dev/null @@ -1,83 +0,0 @@ -import WordPressKit - -class LoginFieldsMeta { - - /// Indicates where the Magic Link Email was sent from. - /// - var emailMagicLinkSource: EmailMagicLinkSource? - - /// Indicates whether a self-hosted user is attempting to log in to Jetpack - /// - var jetpackLogin: Bool - - /// Indicates whether a user is logging in via the wpcom flow or a self-hosted flow. Used by the - /// the LoginFacade in its branching logic. - /// This is a good candidate to refactor out and call the proper login method directly. - /// - var userIsDotCom: Bool - - /// Indicates a wpcom account created via social sign up that requires either a magic link, or a social log in option. - /// If a user signed up via social sign up and subsequently reset their password this field will be false. - /// - var passwordless: Bool - - /// Should point to the site's xmlrpc.php for a self-hosted log in. Should be the value returned via XML-RPC discovery. - /// - var xmlrpcURL: NSURL? - - /// Meta data about a site. This information is fetched and then displayed on the login epilogue. - /// - var siteInfo: WordPressComSiteInfo? - - /// Flags whether a 2FA challenge had to be satisfied before a log in could be complete. - /// Included in analytics after a successful login. - /// - /// A `false` value means that a 2FA prompt was needed. - /// - var requiredMultifactor: Bool - - /// Identifies a social login and the service used. - /// - var socialService: SocialServiceName? - - var socialServiceIDToken: String? - - var socialUser: SocialUser? - - init(emailMagicLinkSource: EmailMagicLinkSource? = nil, - jetpackLogin: Bool = false, - userIsDotCom: Bool = true, - passwordless: Bool = false, - xmlrpcURL: NSURL? = nil, - siteInfo: WordPressComSiteInfo? = nil, - requiredMultifactor: Bool = false, - socialService: SocialServiceName? = nil, - socialServiceIDToken: String? = nil, - socialUser: SocialUser? = nil) { - self.emailMagicLinkSource = emailMagicLinkSource - self.jetpackLogin = jetpackLogin - self.userIsDotCom = userIsDotCom - self.passwordless = passwordless - self.xmlrpcURL = xmlrpcURL - self.siteInfo = siteInfo - self.requiredMultifactor = requiredMultifactor - self.socialService = socialService - self.socialServiceIDToken = socialServiceIDToken - self.socialUser = socialUser - } -} - -extension LoginFieldsMeta { - func copy() -> LoginFieldsMeta { - .init(emailMagicLinkSource: emailMagicLinkSource, - jetpackLogin: jetpackLogin, - userIsDotCom: userIsDotCom, - passwordless: passwordless, - xmlrpcURL: xmlrpcURL, - siteInfo: siteInfo, - requiredMultifactor: requiredMultifactor, - socialService: socialService, - socialServiceIDToken: socialServiceIDToken, - socialUser: socialUser) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Model/WordPressComSiteInfo.swift b/Sources/WordPressAuthenticator/Helpers/Model/WordPressComSiteInfo.swift deleted file mode 100644 index 2be773ff7711..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Model/WordPressComSiteInfo.swift +++ /dev/null @@ -1,61 +0,0 @@ -import Foundation - -// MARK: - WordPress.com Site Info -// -public class WordPressComSiteInfo { - - /// Site's Name! - /// - public let name: String - - /// Tagline. - /// - public let tagline: String - - /// Public URL. - /// - public let url: String - - /// Indicates if Jetpack is available, or not. - /// - public let hasJetpack: Bool - - /// Indicates if Jetpack is active, or not. - /// - public let isJetpackActive: Bool - - /// Indicates if Jetpack is connected, or not. - /// - public let isJetpackConnected: Bool - - /// URL of the Site's Blavatar. - /// - public let icon: String - - /// Indicates whether the site is WordPressDotCom, or not. - /// - public let isWPCom: Bool - - /// Inidcates wheter the site is WordPress, or not. - /// - public let isWP: Bool - - /// Inidcates whether the site exists, or not. - /// - public let exists: Bool - - /// Initializes the current SiteInfo instance with a raw dictionary. - /// - public init(remote: [AnyHashable: Any]) { - name = remote["name"] as? String ?? "" - tagline = remote["description"] as? String ?? "" - url = remote["urlAfterRedirects"] as? String ?? "" - hasJetpack = remote["hasJetpack"] as? Bool ?? false - isJetpackActive = remote["isJetpackActive"] as? Bool ?? false - isJetpackConnected = remote["isJetpackConnected"] as? Bool ?? false - icon = remote["icon.img"] as? String ?? "" - isWPCom = remote["isWordPressDotCom"] as? Bool ?? false - isWP = remote["isWordPress"] as? Bool ?? false - exists = remote["exists"] as? Bool ?? false - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateBack.swift b/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateBack.swift deleted file mode 100644 index 0608c0da09c4..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateBack.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -/// Navigates back one step. -/// -public struct NavigateBack: NavigationCommand { - public init() {} - public func execute(from: UIViewController?) { - pop(navigationController: from?.navigationController) - } -} - -private extension NavigateBack { - func pop(navigationController: UINavigationController?) { - navigationController?.popViewController(animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterAccount.swift b/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterAccount.swift deleted file mode 100644 index 7c889da602cd..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterAccount.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Foundation -import WordPressShared - -/// Navigates to the unified "Continue with WordPress.com" flow. -/// -public struct NavigateToEnterAccount: NavigationCommand { - private let signInSource: SignInSource - private let email: String? - - public init(signInSource: SignInSource, email: String? = nil) { - self.signInSource = signInSource - self.email = email - } - - public func execute(from: UIViewController?) { - continueWithDotCom(email: email, navigationController: from?.navigationController) - } -} - -private extension NavigateToEnterAccount { - private func continueWithDotCom(email: String? = nil, navigationController: UINavigationController?) { - guard let vc = GetStartedViewController.instantiate(from: .getStarted) else { - WPLogError("Failed to navigate from LoginPrologueViewController to GetStartedViewController") - return - } - vc.source = signInSource - vc.loginFields.username = email ?? "" - - navigationController?.pushViewController(vc, animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterSite.swift b/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterSite.swift deleted file mode 100644 index b40742f6105d..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterSite.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation -import WordPressShared - -/// Navigates to the unified site address login flow. -/// -public struct NavigateToEnterSite: NavigationCommand { - public init() {} - public func execute(from: UIViewController?) { - presentUnifiedSiteAddressView(navigationController: from?.navigationController) - } -} - -private extension NavigateToEnterSite { - func presentUnifiedSiteAddressView(navigationController: UINavigationController?) { - guard let vc = SiteAddressViewController.instantiate(from: .siteAddress) else { - WPLogError("Failed to navigate from LoginViewController to SiteAddressViewController") - return - } - - navigationController?.pushViewController(vc, animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterSiteCredentials.swift b/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterSiteCredentials.swift deleted file mode 100644 index e2eb6285bbbb..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterSiteCredentials.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import WordPressShared - -/// Navigates to the wp-admin site credentials flow. -/// -public struct NavigateToEnterSiteCredentials: NavigationCommand { - private let loginFields: LoginFields - - public init(loginFields: LoginFields) { - self.loginFields = loginFields - } - public func execute(from: UIViewController?) { - let navigationController = (from as? UINavigationController) ?? from?.navigationController - presentSiteCredentialsView(navigationController: navigationController, - loginFields: loginFields) - } -} - -private extension NavigateToEnterSiteCredentials { - func presentSiteCredentialsView(navigationController: UINavigationController?, loginFields: LoginFields) { - guard let controller = SiteCredentialsViewController.instantiate(from: .siteAddress) else { - WPLogError("Failed to navigate to SiteCredentialsViewController") - return - } - - controller.loginFields = loginFields - navigationController?.pushViewController(controller, animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterWPCOMPassword.swift b/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterWPCOMPassword.swift deleted file mode 100644 index 174b36dbe50e..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterWPCOMPassword.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import WordPressShared - -/// Navigates to the WPCOM password flow. -/// -public struct NavigateToEnterWPCOMPassword: NavigationCommand { - private let loginFields: LoginFields - - public init(loginFields: LoginFields) { - self.loginFields = loginFields - } - public func execute(from: UIViewController?) { - let navigationController = (from as? UINavigationController) ?? from?.navigationController - presentPasswordView(navigationController: navigationController, - loginFields: loginFields) - } -} - -private extension NavigateToEnterWPCOMPassword { - func presentPasswordView(navigationController: UINavigationController?, loginFields: LoginFields) { - guard let controller = PasswordViewController.instantiate(from: .password) else { - WPLogError("Failed to navigate to PasswordViewController from GetStartedViewController") - return - } - - controller.loginFields = loginFields - navigationController?.pushViewController(controller, animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToRoot.swift b/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToRoot.swift deleted file mode 100644 index a4c6fd48e89d..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToRoot.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -/// Navigates to the root of the unified login flow. -/// -public struct NavigateToRoot: NavigationCommand { - public init() {} - public func execute(from: UIViewController?) { - presentUnifiedSiteAddressView(navigationController: from?.navigationController) - } -} - -private extension NavigateToRoot { - func presentUnifiedSiteAddressView(navigationController: UINavigationController?) { - navigationController?.popToRootViewController(animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigationCommand.swift b/Sources/WordPressAuthenticator/Helpers/Navigation/NavigationCommand.swift deleted file mode 100644 index f6653c571d69..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigationCommand.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation - -/// NavigationCommand abstracts logic necessary provide clients of this library -/// with a way to navigate to a particular location in the UL navigation flow. -/// -/// Concrete implementations of this protocol will decide what that means -/// -public protocol NavigationCommand { - func execute(from: UIViewController?) -} diff --git a/Sources/WordPressAuthenticator/Helpers/SafariCredentialsService.swift b/Sources/WordPressAuthenticator/Helpers/SafariCredentialsService.swift deleted file mode 100644 index 5cee52fae30f..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/SafariCredentialsService.swift +++ /dev/null @@ -1,92 +0,0 @@ -import Foundation -import WordPressShared - -// MARK: - SafariCredentialsService -// -class SafariCredentialsService { - - @objc static let LoginSharedWebCredentialFQDN: CFString = "wordpress.com" as CFString - typealias SharedWebCredentialsCallback = (_ credentialsFound: Bool, _ username: String?, _ password: String?) -> Void - - /// Update safari stored credentials. - /// - /// - Parameter loginFields: An instance of LoginFields - /// - class func updateSafariCredentialsIfNeeded(with loginFields: LoginFields) { - // Paranioa. Don't try and update credentials for self-hosted. - if !loginFields.meta.userIsDotCom { - return - } - - // If the user changed screen names, don't try and update/create a new shared web credential. - // We'll let Safari handle creating newly saved usernames/passwords. - if loginFields.storedCredentials?.storedUserameHash != loginFields.username.hash { - return - } - - // If the user didn't change the password from previousl filled password no update is needed. - if loginFields.storedCredentials?.storedPasswordHash == loginFields.password.hash { - return - } - - // Update the shared credential - let username: CFString = loginFields.username as CFString - let password: CFString = loginFields.password as CFString - - SecAddSharedWebCredential(LoginSharedWebCredentialFQDN, username, password, { (error: CFError?) in - guard error == nil else { - let err = error - WPLogError("Error occurred updating shared web credential: \(String(describing: err?.localizedDescription))") - return - } - DispatchQueue.main.async(execute: { - WordPressAuthenticator.track(.loginAutoFillCredentialsUpdated) - }) - }) - } - - /// Request shared safari credentials if they exist. - /// - /// - Parameter completion: A completion block. - /// - class func requestSharedWebCredentials(_ completion: @escaping SharedWebCredentialsCallback) { - SecRequestSharedWebCredential(LoginSharedWebCredentialFQDN, nil, { (credentials: CFArray?, error: CFError?) in - WPLogInfo("Completed requesting shared web credentials") - guard error == nil else { - let err = error as Error? - if let error = err as NSError?, error.code == -25300 { - // An OSStatus of -25300 is expected when no saved credentails are found. - WPLogInfo("No shared web credenitals found.") - } else { - WPLogError("Error requesting shared web credentials: \(String(describing: err?.localizedDescription))") - } - DispatchQueue.main.async { - completion(false, nil, nil) - } - return - } - - guard let credentials, CFArrayGetCount(credentials) > 0 else { - // Saved credentials exist but were not selected. - DispatchQueue.main.async(execute: { - completion(true, nil, nil) - }) - return - } - - // What a chore! - let unsafeCredentials = CFArrayGetValueAtIndex(credentials, 0) - let credentialsDict = unsafeBitCast(unsafeCredentials, to: CFDictionary.self) - - let unsafeUsername = CFDictionaryGetValue(credentialsDict, Unmanaged.passUnretained(kSecAttrAccount).toOpaque()) - let usernameStr = unsafeBitCast(unsafeUsername, to: CFString.self) as String - - let unsafePassword = CFDictionaryGetValue(credentialsDict, Unmanaged.passUnretained(kSecSharedPassword).toOpaque()) - let passwordStr = unsafeBitCast(unsafePassword, to: CFString.self) as String - - DispatchQueue.main.async { - completion(true, usernameStr, passwordStr) - } - }) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/SignupService.swift b/Sources/WordPressAuthenticator/Helpers/SignupService.swift deleted file mode 100644 index 1ea1498b6339..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/SignupService.swift +++ /dev/null @@ -1,134 +0,0 @@ -import Foundation -import WordPressShared -import WordPressKit - -/// SignupService: Responsible for creating a new WPCom user and blog. -/// -class SignupService: SocialUserCreating { - - /// Create a new WPcom account using Google signin token - /// - /// - Parameters: - /// - googleToken: the token from a successful Google login - /// - success: block called when account is created successfully - /// - failure: block called when account creation fails - /// - func createWPComUserWithGoogle(token: String, - success: @escaping (_ newAccount: Bool, _ username: String, _ wpcomToken: String) -> Void, - failure: @escaping (_ error: Error) -> Void) { - - let remote = WordPressComServiceRemote(wordPressComRestApi: anonymousAPI) - - remote.createWPComAccount(withGoogle: token, - andClientID: configuration.wpcomClientId, - andClientSecret: configuration.wpcomSecret, - success: { response in - - guard let username = response?[ResponseKeys.username] as? String, - let bearer_token = response?[ResponseKeys.bearerToken] as? String else { - failure(SignupError.unknown) - return - } - - let createdAccount = (response?[ResponseKeys.createdAccount] as? Int ?? 0) == 1 - success(createdAccount, username, bearer_token) - }, failure: { error in - failure(error ?? SignupError.unknown) - }) - } - - /// Create a new WPcom account using Apple ID - /// - /// - Parameters: - /// - token: Token provided by Apple. - /// - email: Email provided by Apple. - /// - fullName: Formatted full name provided by Apple. - /// - success: Block called when account is created successfully. - /// - failure: Block called when account creation fails. - /// - func createWPComUserWithApple(token: String, - email: String, - fullName: String?, - success: @escaping (_ newAccount: Bool, - _ existingNonSocialAccount: Bool, - _ existing2faAccount: Bool, - _ username: String, - _ wpcomToken: String) -> Void, - failure: @escaping (_ error: Error) -> Void) { - let remote = WordPressComServiceRemote(wordPressComRestApi: anonymousAPI) - - remote.createWPComAccount(withApple: token, - andEmail: email, - andFullName: fullName, - andClientID: configuration.wpcomClientId, - andClientSecret: configuration.wpcomSecret, - success: { response in - guard let username = response?[ResponseKeys.username] as? String, - let bearer_token = response?[ResponseKeys.bearerToken] as? String else { - failure(SignupError.unknown) - return - } - - let createdAccount = (response?[ResponseKeys.createdAccount] as? Int ?? 0) == 1 - success(createdAccount, false, false, username, bearer_token) - }, failure: { error in - if let error = (error as NSError?) { - - if (error.userInfo[ErrorKeys.errorCode] as? String ?? "") == ErrorKeys.twoFactorEnabled { - success(false, true, true, "", "") - return - } - - if (error.userInfo[ErrorKeys.errorCode] as? String ?? "") == ErrorKeys.existingNonSocialUser { - - // If an account already exists, the account email should be returned in the Error response. - // Extract it and return it. - var existingEmail = "" - if let errorData = error.userInfo[WordPressComRestApi.ErrorKeyErrorData] as? [String: String] { - let emailDict = errorData.first { $0.key == WordPressComRestApi.ErrorKeyErrorDataEmail } - let email = emailDict?.value ?? "" - existingEmail = email - } - - success(false, true, false, existingEmail, "") - return - } - } - - failure(error ?? SignupError.unknown) - }) - } -} - -// MARK: - Private -// -private extension SignupService { - - var anonymousAPI: WordPressComRestApi { - return WordPressComRestApi(oAuthToken: nil, - userAgent: configuration.userAgent, - baseURL: configuration.wpcomAPIBaseURL) - } - - var configuration: WordPressAuthenticatorConfiguration { - return WordPressAuthenticator.shared.configuration - } - - struct ResponseKeys { - static let bearerToken = "bearer_token" - static let username = "username" - static let createdAccount = "created_account" - } - - struct ErrorKeys { - static let errorCode = "WordPressComRestApiErrorCodeKey" - static let existingNonSocialUser = "user_exists" - static let twoFactorEnabled = "2FA_enabled" - } -} - -// MARK: - Errors -// -enum SignupError: Error { - case unknown -} diff --git a/Sources/WordPressAuthenticator/Helpers/SocialUser.swift b/Sources/WordPressAuthenticator/Helpers/SocialUser.swift deleted file mode 100644 index 372287ddde87..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/SocialUser.swift +++ /dev/null @@ -1,8 +0,0 @@ -import WordPressKit - -public struct SocialUser { - - public let email: String - public let fullName: String - public let service: SocialServiceName -} diff --git a/Sources/WordPressAuthenticator/Helpers/SocialUserCreating.swift b/Sources/WordPressAuthenticator/Helpers/SocialUserCreating.swift deleted file mode 100644 index 63b1b6942460..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/SocialUserCreating.swift +++ /dev/null @@ -1,23 +0,0 @@ -/// A type that can create WordPress.com users given a social users, either coming from Google or Apple. -protocol SocialUserCreating: AnyObject { - - func createWPComUserWithGoogle( - token: String, - success: @escaping (_ newAccount: Bool, _ username: String, _ wpcomToken: String) -> Void, - failure: @escaping (_ error: Error) -> Void - ) - - func createWPComUserWithApple( - token: String, - email: String, - fullName: String?, - success: @escaping ( - _ newAccount: Bool, - _ existingNonSocialAccount: Bool, - _ existing2faAccount: Bool, - _ username: String, - _ wpcomToken: String - ) -> Void, - failure: @escaping (_ error: Error) -> Void - ) -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/GoogleAuthenticator.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/GoogleAuthenticator.swift deleted file mode 100644 index 28d5b498e220..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/GoogleAuthenticator.swift +++ /dev/null @@ -1,442 +0,0 @@ -import Foundation -import SVProgressHUD -import WordPressKit -import WordPressShared - -/// Contains delegate methods for Google authentication unified auth flow. -/// Both Login and Signup are handled via this delegate. -/// -protocol GoogleAuthenticatorDelegate: AnyObject { - // Google account login was successful. - func googleFinishedLogin(credentials: AuthenticatorCredentials, loginFields: LoginFields) - - // Google account login was successful, but a WP 2FA code is required. - func googleNeedsMultifactorCode(loginFields: LoginFields) - - // Google account login was successful, but a WP password is required. - func googleExistingUserNeedsConnection(loginFields: LoginFields) - - // Google account login failed. - func googleLoginFailed(errorTitle: String, errorDescription: String, loginFields: LoginFields, unknownUser: Bool) - - // Google account selection cancelled by user. - func googleAuthCancelled() - - // Google account signup was successful. - func googleFinishedSignup(credentials: AuthenticatorCredentials, loginFields: LoginFields) - - // Google account signup redirected to login was successful. - func googleLoggedInInstead(credentials: AuthenticatorCredentials, loginFields: LoginFields) - - // Google account signup failed. - func googleSignupFailed(error: Error, loginFields: LoginFields) -} - -/// Indicate which type of authentication is initiated. -/// Utilized by ViewControllers that handle separate Google Login and Signup flows. -/// This is needed as long as: -/// Separate Google Login and Signup flows are utilized. -/// Tracking is specific to separate Login and Signup flows. -/// When separate Google Login and Signup flows are no longer used, this no longer needed. -/// -enum GoogleAuthType { - case login - case signup -} - -/// Contains delegate methods for Google login specific flow. -/// When separate Google Login and Signup flows are no longer used, this no longer needed. -/// -protocol GoogleAuthenticatorLoginDelegate: AnyObject { - // Google account login was successful. - func googleFinishedLogin(credentials: AuthenticatorCredentials, loginFields: LoginFields) - - // Google account login was successful, but a WP 2FA code is required. - func googleNeedsMultifactorCode(loginFields: LoginFields) - - // Google account login was successful, but a WP password is required. - func googleExistingUserNeedsConnection(loginFields: LoginFields) - - // Google account login failed. - func googleLoginFailed(errorTitle: String, errorDescription: String, loginFields: LoginFields) -} - -/// Contains delegate methods for Google signup specific flow. -/// When separate Google Login and Signup flows are no longer used, this no longer needed. -/// -protocol GoogleAuthenticatorSignupDelegate: AnyObject { - // Google account signup was successful. - func googleFinishedSignup(credentials: AuthenticatorCredentials, loginFields: LoginFields) - - // Google account signup redirected to login was successful. - func googleLoggedInInstead(credentials: AuthenticatorCredentials, loginFields: LoginFields) - - // Google account signup failed. - func googleSignupFailed(error: Error, loginFields: LoginFields) - - // Google account signup cancelled by user. - func googleSignupCancelled() -} - -class GoogleAuthenticator: NSObject { - - // MARK: - Properties - - static var sharedInstance = GoogleAuthenticator() - weak var loginDelegate: GoogleAuthenticatorLoginDelegate? - weak var signupDelegate: GoogleAuthenticatorSignupDelegate? - weak var delegate: GoogleAuthenticatorDelegate? - - private var loginFields = LoginFields() - private let authConfig = WordPressAuthenticator.shared.configuration - private var authType: GoogleAuthType = .login - - private var tracker: AuthenticatorAnalyticsTracker { - AuthenticatorAnalyticsTracker.shared - } - - private lazy var loginFacade: LoginFacade = { - let facade = LoginFacade(dotcomClientID: authConfig.wpcomClientId, - dotcomSecret: authConfig.wpcomSecret, - userAgent: authConfig.userAgent) - facade.delegate = self - return facade - }() - - private weak var authenticationDelegate: WordPressAuthenticatorDelegate? = { - guard let delegate = WordPressAuthenticator.shared.delegate else { - fatalError() - } - return delegate - }() - - // MARK: - Start Authentication - - /// Public method to initiate the Google auth process. - /// - Parameters: - /// - viewController: The UIViewController that Google is being presented from. - /// Required by Google SDK. - /// - loginFields: LoginFields from the calling view controller. - /// The values are updated during the Google process, - /// and returned to the calling view controller via delegate methods. - /// - authType: Indicates the type of authentication (login or signup) - func showFrom( - viewController: UIViewController, - loginFields: LoginFields, - for authType: GoogleAuthType = .login - ) { - // The fact that we set `loginFields`, then reset its `meta.socialService` property doesn't - // seem ideal... - self.loginFields = loginFields - self.loginFields.meta.socialService = SocialServiceName.google - self.authType = authType - - Task { @MainActor in - do { - let token = try await requestAuthorization( - for: authType, - from: viewController, - loginFields: loginFields - ) - - didSignIn(token: token.token.rawValue, email: token.email, fullName: token.name) - } catch { - failedToSignIn(error: error) - } - } - } - - /// Public method to create a WP account with a Google account. - /// - Parameters: - /// - loginFields: LoginFields from the calling view controller. - /// The values are updated during the Google process, - /// and returned to the calling view controller via delegate methods. - func createGoogleAccount(loginFields: LoginFields) { - self.loginFields = loginFields - - guard let token = loginFields.meta.socialServiceIDToken else { - WPLogError("GoogleAuthenticator - createGoogleAccount: Failed to get Google account information.") - return - } - - createWordPressComUser(token: token, email: loginFields.emailAddress) - } -} - -// MARK: - Private Extension - -private extension GoogleAuthenticator { - - private func trackRequestAuthorizitation(type: GoogleAuthType) { - switch type { - case .login: - tracker.set(flow: .loginWithGoogle) - tracker.track(step: .start) { - track(.loginSocialButtonClick) - } - case .signup: - track(.createAccountInitiated) - } - } - - func track(_ event: WPAnalyticsStat, properties: [AnyHashable: Any] = [:]) { - var trackProperties = properties - trackProperties["source"] = "google" - WordPressAuthenticator.track(event, properties: trackProperties) - } - - private func failedToSignIn(error: Error?) { - // The Google SignIn may have been cancelled. - // - // FIXME: Is `error == .none` how we distinguish between user cancellation and legit error? - let failure = error?.localizedDescription ?? "Unknown error" - - tracker.track(failure: failure, ifTrackingNotEnabled: { - let properties = ["error": failure] - - switch authType { - case .login: - track(.loginSocialButtonFailure, properties: properties) - case .signup: - track(.signupSocialButtonFailure, properties: properties) - } - }) - - // Notify the delegates so the Google Auth view can be dismissed. - // - // FIXME: Shouldn't we be calling a method to report error, if there was one? - signupDelegate?.googleSignupCancelled() - delegate?.googleAuthCancelled() - } - - private func didSignIn(token: String, email: String, fullName: String) { - // Save account information to pass back to delegate later. - loginFields.emailAddress = email - loginFields.username = email - loginFields.meta.socialServiceIDToken = token - loginFields.meta.socialUser = SocialUser(email: email, fullName: fullName, service: .google) - - guard authConfig.enableUnifiedAuth else { - // Initiate separate WP login / signup paths. - switch authType { - case .login: - SVProgressHUD.show() - loginFacade.loginToWordPressDotCom(withSocialIDToken: token, service: SocialServiceName.google.rawValue) - case .signup: - createWordPressComUser(token: token, email: email) - } - - return - } - - // Initiate unified path by attempting to login first. - // - // `SVProgressHUD.show()` will crash in an app that doesn't have a window property in its - // `UIApplicationDelegate`, such as those created via the Xcode templates circa version 12 - // onwards. - SVProgressHUD.show() - loginFacade.loginToWordPressDotCom(withSocialIDToken: token, service: SocialServiceName.google.rawValue) - } - - enum LocalizedText { - static let googleConnected = NSLocalizedString("Connected But…", comment: "Title shown when a user logs in with Google but no matching WordPress.com account is found") - static let googleConnectedError = NSLocalizedString("The Google account \"%@\" doesn't match any account on WordPress.com", comment: "Description shown when a user logs in with Google but no matching WordPress.com account is found") - static let googleUnableToConnect = NSLocalizedString("Unable To Connect", comment: "Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com") - } -} - -// MARK: - SDK-less flow - -extension GoogleAuthenticator { - - private func requestAuthorization( - for authType: GoogleAuthType, - from viewController: UIViewController, - loginFields: LoginFields - ) async throws -> IDToken { - // Intentionally duplicated from the callsite, so we don't forget about this when removing - // the SDK. - // - // The fact that we set `loginFields`, then reset its `meta.socialService` property doesn't - // seem ideal... - self.loginFields = loginFields - self.loginFields.meta.socialService = SocialServiceName.google - self.authType = authType - - trackRequestAuthorizitation(type: authType) - - let sdkLessGoogleAuthenticator = NewGoogleAuthenticator( - clientId: authConfig.googleClientId, - scheme: authConfig.googleLoginScheme, - audience: authConfig.googleLoginServerClientId, - urlSession: .shared - ) - - await SVProgressHUD.show() - return try await sdkLessGoogleAuthenticator.getOAuthToken(from: viewController) - } -} - -// MARK: - LoginFacadeDelegate - -extension GoogleAuthenticator: LoginFacadeDelegate { - - // Google account login was successful. - func finishedLogin(withGoogleIDToken googleIDToken: String, authToken: String) { - SVProgressHUD.dismiss() - - // This stat is part of a funnel that provides critical information. Please - // consult with your lead before removing this event. - track(.signedIn) - - if tracker.shouldUseLegacyTracker() { - track(.loginSocialSuccess) - } - - let wpcom = WordPressComCredentials(authToken: authToken, - isJetpackLogin: loginFields.meta.jetpackLogin, - multifactor: false, - siteURL: loginFields.siteAddress) - let credentials = AuthenticatorCredentials(wpcom: wpcom) - - loginDelegate?.googleFinishedLogin(credentials: credentials, loginFields: loginFields) - delegate?.googleFinishedLogin(credentials: credentials, loginFields: loginFields) - } - - // Google account login was successful, but a WP 2FA code is required. - func needsMultifactorCode(forUserID userID: Int, andNonceInfo nonceInfo: SocialLogin2FANonceInfo) { - SVProgressHUD.dismiss() - - loginFields.nonceInfo = nonceInfo - loginFields.nonceUserID = userID - - if tracker.shouldUseLegacyTracker() { - track(.loginSocial2faNeeded) - } - - loginDelegate?.googleNeedsMultifactorCode(loginFields: loginFields) - delegate?.googleNeedsMultifactorCode(loginFields: loginFields) - } - - // Google account login was successful, but a WP password is required. - func existingUserNeedsConnection(_ email: String) { - SVProgressHUD.dismiss() - - loginFields.username = email - loginFields.emailAddress = email - - if tracker.shouldUseLegacyTracker() { - track(.loginSocialAccountsNeedConnecting) - } - - loginDelegate?.googleExistingUserNeedsConnection(loginFields: loginFields) - delegate?.googleExistingUserNeedsConnection(loginFields: loginFields) - } - - // Google account login failed. - func displayRemoteError(_ error: Error) { - SVProgressHUD.dismiss() - - var errorTitle = LocalizedText.googleUnableToConnect - var errorDescription = error.localizedDescription - let unknownUser = (error as? WordPressComOAuthError)?.authenticationFailureKind == .unknownUser - - if unknownUser { - errorTitle = LocalizedText.googleConnected - errorDescription = String(format: LocalizedText.googleConnectedError, loginFields.username) - - if tracker.shouldUseLegacyTracker() { - track(.loginSocialErrorUnknownUser) - } - } else { - // Don't track unknown user for unified Auth. - tracker.track(failure: errorDescription) - } - - loginDelegate?.googleLoginFailed(errorTitle: errorTitle, errorDescription: errorDescription, loginFields: loginFields) - delegate?.googleLoginFailed(errorTitle: errorTitle, errorDescription: errorDescription, loginFields: loginFields, unknownUser: unknownUser) - } -} - -// MARK: - Sign Up Methods - -private extension GoogleAuthenticator { - - /// Creates a WordPress.com account with the associated Google token and email. - /// - func createWordPressComUser(token: String, email: String) { - SVProgressHUD.show() - let service = SignupService() - - service.createWPComUserWithGoogle(token: token, success: { [weak self] accountCreated, wpcomUsername, wpcomToken in - - let wpcom = WordPressComCredentials(authToken: wpcomToken, isJetpackLogin: false, multifactor: false, siteURL: self?.loginFields.siteAddress ?? "") - let credentials = AuthenticatorCredentials(wpcom: wpcom) - - // New Account - if accountCreated { - SVProgressHUD.dismiss() - // Notify the host app - self?.authenticationDelegate?.createdWordPressComAccount(username: wpcomUsername, authToken: wpcomToken) - // Notify the delegate - self?.accountCreated(credentials: credentials) - - return - } - - // Existing Account - // Sync host app - self?.authenticationDelegate?.sync(credentials: credentials) { - SVProgressHUD.dismiss() - // Notify delegate - self?.logInInstead(credentials: credentials) - } - }, failure: { [weak self] error in - SVProgressHUD.dismiss() - // Notify delegate - self?.signupFailed(error: error) - }) - } - - func accountCreated(credentials: AuthenticatorCredentials) { - // This stat is part of a funnel that provides critical information. Before - // making ANY modification to this stat please refer to: p4qSXL-35X-p2 - track(.createdAccount) - - // This stat is part of a funnel that provides critical information. Please - // consult with your lead before removing this event. - track(.signedIn) - - tracker.track(step: .success, ifTrackingNotEnabled: { - track(.signupSocialSuccess) - }) - - signupDelegate?.googleFinishedSignup(credentials: credentials, loginFields: loginFields) - delegate?.googleFinishedSignup(credentials: credentials, loginFields: loginFields) - } - - func logInInstead(credentials: AuthenticatorCredentials) { - tracker.set(flow: .loginWithGoogle) - - // This stat is part of a funnel that provides critical information. Please - // consult with your lead before removing this event. - track(.signedIn) - - tracker.track(step: .start) { - track(.signupSocialToLogin) - track(.loginSocialSuccess) - } - - signupDelegate?.googleLoggedInInstead(credentials: credentials, loginFields: loginFields) - delegate?.googleLoggedInInstead(credentials: credentials, loginFields: loginFields) - } - - func signupFailed(error: Error) { - tracker.track(failure: error.localizedDescription, ifTrackingNotEnabled: { - track(.signupSocialFailure, properties: ["error": error.localizedDescription]) - }) - - signupDelegate?.googleSignupFailed(error: error, loginFields: loginFields) - delegate?.googleSignupFailed(error: error, loginFields: loginFields) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/StoredCredentialsAuthenticator.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/StoredCredentialsAuthenticator.swift deleted file mode 100644 index 6d01ca653687..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/StoredCredentialsAuthenticator.swift +++ /dev/null @@ -1,247 +0,0 @@ -import Foundation -import AuthenticationServices -import SVProgressHUD -import WordPressKit -import WordPressShared - -/// The authorization flow handled by this class starts by showing Apple's `ASAuthorizationController` -/// through our class `StoredCredentialsPicker`. This controller lets the user pick the credentials they -/// want to login with. This class handles both showing that controller and executing the remaining flow to -/// complete the login process. -/// -class StoredCredentialsAuthenticator: NSObject { - - // MARK: - Delegates - - private var authenticationDelegate: WordPressAuthenticatorDelegate { - guard let delegate = WordPressAuthenticator.shared.delegate else { - fatalError() - } - return delegate - } - - // MARK: - Configuration - - private var authConfig: WordPressAuthenticatorConfiguration { - WordPressAuthenticator.shared.configuration - } - - // MARK: - Login Support - - private lazy var loginFacade: LoginFacade = { - let facade = LoginFacade(dotcomClientID: authConfig.wpcomClientId, - dotcomSecret: authConfig.wpcomSecret, - userAgent: authConfig.userAgent) - facade.delegate = self - return facade - }() - - // MARK: - Cancellation - - private let onCancel: (() -> Void)? - - // MARK: - UI - - private let picker = StoredCredentialsPicker() - private weak var navigationController: UINavigationController? - - // MARK: - Tracking Support - - private var tracker: AuthenticatorAnalyticsTracker { - AuthenticatorAnalyticsTracker.shared - } - - // MARK: - Login Fields - - private var loginFields: LoginFields? - - // MARK: - Initialization - - init(onCancel: (() -> Void)? = nil) { - self.onCancel = onCancel - } - - // MARK: - Picker - - /// Shows the UI for picking stored credentials for the user to log into their account. - /// - func showPicker(from navigationController: UINavigationController) { - self.navigationController = navigationController - - guard let window = navigationController.view.window else { - WPLogError("Can't obtain window for navigation controller") - return - } - - picker.show(in: window) { [weak self] result in - guard let self else { - return - } - - switch result { - case .success(let authorization): - self.pickerSuccess(authorization) - case .failure(let error): - self.pickerFailure(error) - } - } - } - - /// The selection of credentials and subsequent authorization by the OS succeeded. This method processes the credentials - /// and proceeds with the login operation. - /// - /// - Parameters: - /// - authorization: The authorization by the OS, containing the credentials picked by the user. - /// - private func pickerSuccess(_ authorization: ASAuthorization) { - tracker.track(step: .start) - tracker.set(flow: .loginWithiCloudKeychain) - SVProgressHUD.show() - - switch authorization.credential { - case _ as ASAuthorizationAppleIDCredential: - // No-op for now, but we can decide to implement AppleID login through this authenticator - // by implementing the logic here. - break - case let credential as ASPasswordCredential: - let loginFields = LoginFields.makeForWPCom(username: credential.user, password: credential.password) - loginFacade.signIn(with: loginFields) - self.loginFields = loginFields - default: - // There aren't any other known methods for us to handle here, but we still need to complete the switch - // statement. - break - } - } - - /// The selection of credentials or the subsequent authorization by the OS failed. This method processes the failure. - /// - /// - Parameters: - /// - error: The error detailing what failed. - /// - private func pickerFailure(_ error: Error) { - let authError = ASAuthorizationError(_nsError: error as NSError) - - switch authError.code { - case .canceled: - // The user cancelling the flow is not really an error, so we're not reporting or tracking - // this as an error. - // - // We're not tracking this either, since the Android App doesn't for SmartLock. The reason is - // that it's not trivial to know when the credentials picker UI is shown to the user, so knowing - // it's being dismissed is also not trivial. This was decided during the Unified Login & Signup - // project in a conversation between myself (Diego Rey Mendez) and Renan Ferrari. - break - default: - tracker.track(failure: authError.localizedDescription) - WPLogError("ASAuthorizationError: \(authError.localizedDescription)") - } - } -} - -extension StoredCredentialsAuthenticator: LoginFacadeDelegate { - func displayRemoteError(_ error: Error) { - tracker.track(failure: error.localizedDescription) - SVProgressHUD.dismiss() - - guard authConfig.enableUnifiedAuth else { - presentLoginEmailView(error: error) - return - } - - presentGetStartedView(error: error) - } - - func needsMultifactorCode() { - SVProgressHUD.dismiss() - presentTwoFactorAuthenticationView() - } - - func needsMultifactorCode(forUserID userID: Int, andNonceInfo nonceInfo: SocialLogin2FANonceInfo) { - loginFields?.nonceInfo = nonceInfo - loginFields?.nonceUserID = userID - - needsMultifactorCode() - } - - func finishedLogin(withAuthToken authToken: String, requiredMultifactorCode: Bool) { - let wpcom = WordPressComCredentials( - authToken: authToken, - isJetpackLogin: false, - multifactor: requiredMultifactorCode, - siteURL: "") - let credentials = AuthenticatorCredentials(wpcom: wpcom) - - authenticationDelegate.sync(credentials: credentials) { [weak self] in - SVProgressHUD.dismiss() - self?.presentLoginEpilogue(credentials: credentials) - } - } -} - -// MARK: - UI Flow - -extension StoredCredentialsAuthenticator { - private func presentLoginEpilogue(credentials: AuthenticatorCredentials) { - guard let navigationController = self.navigationController else { - WPLogError("No navigation controller to present the login epilogue from") - return - } - - authenticationDelegate.presentLoginEpilogue(in: navigationController, - for: credentials, - source: WordPressAuthenticator.shared.signInSource, - onDismiss: {}) - } - - /// Presents the login email screen, displaying the specified error. This is useful - /// for example for iCloud Keychain in the case where there's an error logging the user - /// in with the stored credentials for whatever reason. - /// - private func presentLoginEmailView(error: Error) { - guard let toVC = LoginEmailViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate to LoginEmailVC from LoginPrologueVC") - return - } - - if let loginFields { - toVC.loginFields = loginFields - } - toVC.errorToPresent = error - - navigationController?.pushViewController(toVC, animated: true) - } - - /// Presents the get started screen, displaying the specified error. This is useful - /// for example for iCloud Keychain in the case where there's an error logging the user - /// in with the stored credentials for whatever reason. - /// - private func presentGetStartedView(error: Error) { - guard let toVC = GetStartedViewController.instantiate(from: .getStarted) else { - WPLogError("Failed to navigate to GetStartedViewController") - return - } - - if let loginFields { - toVC.loginFields = loginFields - } - - toVC.errorMessage = error.localizedDescription - navigationController?.pushViewController(toVC, animated: true) - } - - private func presentTwoFactorAuthenticationView() { - guard let loginFields else { - return - } - - guard let vc = TwoFAViewController.instantiate(from: .twoFA) else { - WPLogError("Failed to navigate from LoginViewController to TwoFAViewController") - return - } - - vc.loginFields = loginFields - - navigationController?.pushViewController(vc, animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/StoredCredentialsPicker.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/StoredCredentialsPicker.swift deleted file mode 100644 index a0668f5ab977..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/StoredCredentialsPicker.swift +++ /dev/null @@ -1,55 +0,0 @@ -import Foundation -import AuthenticationServices - -/// Thin wrapper around `ASAuthorizationController` to avoid having to set delegate methods in the VC -/// and to modularize / abstract the logic to show Apple's UI for picking the stored credentials. -/// -/// This picker takes care of returning the credentials that were picked (and authorized by the iOS) through a closure. -/// It's not within the scope of this class to take care of what happens after the credentials are picked. -/// -class StoredCredentialsPicker: NSObject { - - typealias CompletionClosure = (Result) -> Void - - /// The closure that will be executed once the credentials are picked and returned by the OS, - /// or once there's an Error. - /// - private var onComplete: CompletionClosure! - - /// The window where the quick authentication flow will be shown. - /// - private var window: UIWindow! - - func show(in window: UIWindow, onComplete: @escaping CompletionClosure) { - - self.onComplete = onComplete - self.window = window - - let requests = [ASAuthorizationPasswordProvider().createRequest()] - let controller = ASAuthorizationController(authorizationRequests: requests) - - controller.delegate = self - controller.presentationContextProvider = self - controller.performRequests() - } -} - -// MARK: - ASAuthorizationControllerDelegate - -extension StoredCredentialsPicker: ASAuthorizationControllerDelegate { - func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { - onComplete(.success(authorization)) - } - - func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { - onComplete(.failure(error)) - } -} - -// MARK: - ASAuthorizationControllerPresentationContextProviding - -extension StoredCredentialsPicker: ASAuthorizationControllerPresentationContextProviding { - func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - return window - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/2FA/TwoFA.storyboard b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/2FA/TwoFA.storyboard deleted file mode 100644 index 19b8e002cb02..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/2FA/TwoFA.storyboard +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/2FA/TwoFAViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/2FA/TwoFAViewController.swift deleted file mode 100644 index 0e95674e4f7f..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/2FA/TwoFAViewController.swift +++ /dev/null @@ -1,694 +0,0 @@ -import UIKit -import WordPressKit -import WordPressShared -import SVProgressHUD -import AuthenticationServices - -/// TwoFAViewController: view to enter 2FA code. -/// -final class TwoFAViewController: LoginViewController { - - // MARK: - Properties - - @IBOutlet private weak var tableView: UITableView! - @IBOutlet var bottomContentConstraint: NSLayoutConstraint? - private weak var codeField: UITextField? - - private var rows = [Row]() - private var errorMessage: String? - private var pasteboardChangeCountBeforeBackground: Int? - private var shouldChangeVoiceOverFocus: Bool = false - - /// Tracks when the initial challenge request was made. - private var initialChallengeRequestTime: Date? - - override var sourceTag: WordPressSupportSourceTag { - get { - return .login2FA - } - } - - // Required for `NUXKeyboardResponder` but unused here. - var verticalCenterConstraint: NSLayoutConstraint? - - // MARK: - View - - override func viewDidLoad() { - super.viewDidLoad() - - removeGoogleWaitingView() - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.logInTitle - - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - - localizePrimaryButton() - registerTableViewCells() - loadRows() - configureForAccessibility() - } - - override func viewDidAppear(_ animated: Bool) { - - super.viewDidAppear(animated) - - if isMovingToParent { - tracker.track(step: .twoFactorAuthentication) - } else { - tracker.set(step: .twoFactorAuthentication) - } - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - - configureSubmitButton(animating: false) - configureViewForEditingIfNeeded() - - let nc = NotificationCenter.default - nc.addObserver(self, selector: #selector(applicationBecameInactive), name: UIApplication.willResignActiveNotification, object: nil) - nc.addObserver(self, selector: #selector(applicationBecameActive), name: UIApplication.didBecomeActiveNotification, object: nil) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - - // Multifactor codes are time sensitive, so clear the stored code if the - // user dismisses the view. They'll need to reenter it upon return. - loginFields.multifactorCode = "" - codeField?.text = "" - } - - // MARK: - Overrides - - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? - WordPressAuthenticator.shared.style.statusBarStyle - } - - /// Configures the appearance and state of the submit button. - /// - override func configureSubmitButton(animating: Bool) { - submitButton?.showActivityIndicator(animating) - - let isNumeric = loginFields.multifactorCode.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil - let isValidLength = SocialLogin2FANonceInfo.TwoFactorTypeLengths(rawValue: loginFields.multifactorCode.count) != nil - - submitButton?.isEnabled = ( - !animating && - isNumeric && - isValidLength - ) - } - - override func configureViewLoading(_ loading: Bool) { - super.configureViewLoading(loading) - codeField?.isEnabled = !loading - initialChallengeRequestTime = nil - } - - override func displayRemoteError(_ error: Error) { - displayError(message: "") - - let err = error as NSError - - // If the error happened because the security key challenge request started more than 1 minute ago, show a timeout error. - // This check is needed because the server sends a generic error. - if let initialChallengeRequestTime, Date().timeIntervalSince(initialChallengeRequestTime) >= 60, err.code == .zero { - return displaySecurityKeyErrorMessageAndExitFlow(message: LocalizedText.timeoutError) - } - - configureViewLoading(false) - if (error as? WordPressComOAuthError)?.authenticationFailureKind == .invalidOneTimePassword { - // Invalid verification code. - displayError(message: LocalizedText.bad2FAMessage, moveVoiceOverFocus: true) - } else if case let .endpointError(authenticationFailure) = (error as? WordPressComOAuthError), authenticationFailure.kind == .invalidTwoStepCode { - // Invalid 2FA during social login - if let newNonce = authenticationFailure.newNonce { - loginFields.nonceInfo?.updateNonce(with: newNonce) - } - displayError(message: LocalizedText.bad2FAMessage, moveVoiceOverFocus: true) - } else { - displayError(error, sourceTag: sourceTag) - } - } - - override func displayError(message: String, moveVoiceOverFocus: Bool = false) { - if errorMessage != message { - if !message.isEmpty { - tracker.track(failure: message) - } - - errorMessage = message - shouldChangeVoiceOverFocus = moveVoiceOverFocus - loadRows() - tableView.reloadData() - } - } -} - -// MARK: - Validation and Login - -private extension TwoFAViewController { - - // MARK: - Button Actions - - @IBAction func handleContinueButtonTapped(_ sender: NUXButton) { - tracker.track(click: .submitTwoFactorCode) - validateForm() - } - - func requestCode() { - SVProgressHUD.showSuccess(withStatus: LocalizedText.smsSent) - SVProgressHUD.dismiss(withDelay: TimeInterval(1)) - - if loginFields.nonceInfo != nil { - // social login - loginFacade.requestSocial2FACode(with: loginFields) - } else { - loginFacade.requestOneTimeCode(with: loginFields) - } - } - - // MARK: - Login - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with the submit action. - /// - func validateForm() { - guard let nonceInfo = loginFields.nonceInfo else { - return validateFormAndLogin() - } - - let (authType, nonce) = nonceInfo.authTypeAndNonce(for: loginFields.multifactorCode) - if nonce.isEmpty { - return validateFormAndLogin() - } - - loginWithNonce(nonce, authType: authType, code: loginFields.multifactorCode) - } - - func loginWithNonce(_ nonce: String, authType: String, code: String) { - configureViewLoading(true) - loginFacade.loginToWordPressDotCom(withUser: loginFields.nonceUserID, authType: authType, twoStepCode: code, twoStepNonce: nonce) - } - - func finishedLogin(withNonceAuthToken authToken: String) { - let wpcom = WordPressComCredentials(authToken: authToken, isJetpackLogin: isJetpackLogin, multifactor: true, siteURL: loginFields.siteAddress) - let credentials = AuthenticatorCredentials(wpcom: wpcom) - syncWPComAndPresentEpilogue(credentials: credentials) - } - - // MARK: - Security Keys - - func loginWithSecurityKeys() { - - guard let twoStepNonce = loginFields.nonceInfo?.nonceWebauthn else { - return displaySecurityKeyErrorMessageAndExitFlow() - } - - configureViewLoading(true) - initialChallengeRequestTime = Date() - - Task { @MainActor in - guard let challengeInfo = await loginFacade.requestWebauthnChallenge(userID: loginFields.nonceUserID, twoStepNonce: twoStepNonce) else { - return displaySecurityKeyErrorMessageAndExitFlow() - } - - signChallenge(challengeInfo) - } - } - - func signChallenge(_ challengeInfo: WebauthnChallengeInfo) { - - loginFields.nonceInfo?.updateNonce(with: challengeInfo.twoStepNonce) - loginFields.webauthnChallengeInfo = challengeInfo - - let challenge = Data(base64URLEncoded: challengeInfo.challenge) ?? Data() - let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: challengeInfo.rpID) - let platformKeyRequest = platformProvider.createCredentialAssertionRequest(challenge: challenge) - - let authController = ASAuthorizationController(authorizationRequests: [platformKeyRequest]) - authController.delegate = self - authController.presentationContextProvider = self - authController.performRequests() - } - - // When an security key error occurs, we need to restart the flow to regenerate the necessary nonces. - func displaySecurityKeyErrorMessageAndExitFlow(message: String = LocalizedText.unknownError) { - configureViewLoading(false) - displayErrorAlert(message, sourceTag: .loginWebauthn, onDismiss: { [weak self] in - self?.navigationController?.popViewController(animated: true) - }) - } - - // MARK: - Code Validation - - enum CodeValidation { - case invalid(nonNumbers: Bool) - case valid(String) - } - - func isValidCode(code: String) -> CodeValidation { - let codeStripped = code.components(separatedBy: .whitespacesAndNewlines).joined() - let allowedCharacters = CharacterSet.decimalDigits - let resultCharacterSet = CharacterSet(charactersIn: codeStripped) - let isOnlyNumbers = allowedCharacters.isSuperset(of: resultCharacterSet) - let isShortEnough = codeStripped.count <= SocialLogin2FANonceInfo.TwoFactorTypeLengths.backup.rawValue - - if isOnlyNumbers && isShortEnough { - return .valid(codeStripped) - } - - if isOnlyNumbers { - return .invalid(nonNumbers: false) - } - - return .invalid(nonNumbers: true) - } - - // MARK: - Text Field Handling - - func handleTextFieldDidChange(_ sender: UITextField) { - loginFields.multifactorCode = codeField?.nonNilTrimmedText() ?? "" - configureSubmitButton(animating: false) - } -} - -// MARK: - Security Keys -extension TwoFAViewController: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { - - func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { - - // Validate necessary data - guard let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion, - let challengeInfo = loginFields.webauthnChallengeInfo, - let clientDataJson = extractClientData(from: credential, challengeInfo: challengeInfo) else { - return displaySecurityKeyErrorMessageAndExitFlow() - } - - // Validate that the submitted passkey is allowed. - guard challengeInfo.allowedCredentialIDs.contains(credential.credentialID.base64URLEncodedString()) else { - return displaySecurityKeyErrorMessageAndExitFlow(message: LocalizedText.invalidKey) - } - - loginFacade.authenticateWebauthnSignature(userID: loginFields.nonceUserID, - twoStepNonce: challengeInfo.twoStepNonce, - credentialID: credential.credentialID, - clientDataJson: clientDataJson, - authenticatorData: credential.rawAuthenticatorData, - signature: credential.signature, - userHandle: credential.userID) - } - - // Some password managers(like 1P) don't deliver `rawClientDataJSON`. In those cases we need to assemble it manually. - func extractClientData(from credential: ASAuthorizationPlatformPublicKeyCredentialAssertion, challengeInfo: WebauthnChallengeInfo) -> Data? { - - if !credential.rawClientDataJSON.isEmpty { - return credential.rawClientDataJSON - } - - // We build this manually because we need to guarantee this exact element order. - let rawClientJSON = "{\"type\":\"webauthn.get\",\"challenge\":\"\(challengeInfo.challenge)\",\"origin\":\"https://\(challengeInfo.rpID)\"}" - return rawClientJSON.data(using: .utf8) - } - - func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { - WPLogError("Error signing challenge: \(error.localizedDescription)") - displaySecurityKeyErrorMessageAndExitFlow() - } - - func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - view.window! - } -} - -// MARK: - UITextFieldDelegate - -extension TwoFAViewController: UITextFieldDelegate { - - /// Only allow digits in the 2FA text field - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString: String) -> Bool { - - guard let fieldText = textField.text as NSString? else { - return true - } - - let resultString = fieldText.replacingCharacters(in: range, with: replacementString) - - switch isValidCode(code: resultString) { - case .valid(let cleanedCode): - displayError(message: "") - - // because the string was stripped of whitespace, we can't return true and we update the textfield ourselves - textField.text = cleanedCode - handleTextFieldDidChange(textField) - case .invalid(nonNumbers: true): - displayError(message: LocalizedText.numericalCode) - default: - if let pasteString = UIPasteboard.general.string, pasteString == replacementString { - displayError(message: LocalizedText.invalidCode) - } - } - - return false - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - validateForm() - return true - } -} - -// MARK: - UITableViewDataSource - -extension TwoFAViewController: UITableViewDataSource { - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return rows.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - return cell - } -} - -// MARK: - Keyboard Notifications - -extension TwoFAViewController: NUXKeyboardResponder { - - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } -} - -// MARK: - Application state changes - -private extension TwoFAViewController { - - @objc func applicationBecameInactive() { - pasteboardChangeCountBeforeBackground = UIPasteboard.general.changeCount - } - - @objc func applicationBecameActive() { - guard let codeField else { - return - } - - let emptyField = codeField.text?.isEmpty ?? true - guard emptyField, - pasteboardChangeCountBeforeBackground != UIPasteboard.general.changeCount else { - return - } - - UIPasteboard.general.detectAuthenticatorCode { [weak self] result in - switch result { - case .success(let authenticatorCode): - self?.handle(code: authenticatorCode, textField: codeField) - case .failure: - break - } - } - } - - private func handle(code: String, textField: UITextField) { - switch isValidCode(code: code) { - case .valid(let cleanedCode): - displayError(message: "") - textField.text = cleanedCode - handleTextFieldDidChange(textField) - default: - break - } - } -} - -// MARK: - Table Management - -private extension TwoFAViewController { - - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib(), - TextFieldTableViewCell.reuseIdentifier: TextFieldTableViewCell.loadNib(), - TextLinkButtonTableViewCell.reuseIdentifier: TextLinkButtonTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - - tableView.register(SpacerTableViewCell.self, forCellReuseIdentifier: SpacerTableViewCell.reuseIdentifier) - } - - /// Describes how the tableView rows should be rendered. - /// - func loadRows() { - rows = [.instructions, .code] - - if let errorText = errorMessage, !errorText.isEmpty { - rows.append(.errorMessage) - } - - rows.append(.spacer(20)) - rows.append(.alternateInstructions) - - rows.append(.spacer(4)) - rows.append(.sendCode) - - if WordPressAuthenticator.shared.configuration.enablePasskeys, loginFields.nonceInfo?.nonceWebauthn.isEmpty == false { - rows.append(.spacer(4)) - rows.append(.enterSecurityKey) - } - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextLabelTableViewCell where row == .alternateInstructions: - configureAlternateInstructionLabel(cell) - case let cell as TextFieldTableViewCell: - configureTextField(cell) - case let cell as TextLinkButtonTableViewCell where row == .sendCode: - configureTextLinkButton(cell) - case let cell as TextLinkButtonTableViewCell where row == .enterSecurityKey: - configureEnterSecurityKeyLinkButton(cell) - case let cell as TextLabelTableViewCell where row == .errorMessage: - configureErrorLabel(cell) - case let cell as SpacerTableViewCell: - if case let .spacer(spacing) = row { - configureSpacerCell(cell, spacing: spacing) - } - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the instruction cell. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.twoFactorInstructions) - } - - /// Configure the alternate instruction cell. - /// - func configureAlternateInstructionLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.twoFactorOtherFormsInstructions) - } - - /// Configure the textfield cell. - /// - func configureTextField(_ cell: TextFieldTableViewCell) { - cell.configure(withStyle: .numericCode, - placeholder: WordPressAuthenticator.shared.displayStrings.twoFactorCodePlaceholder) - - // Save a reference to the first textField so it can becomeFirstResponder. - codeField = cell.textField - cell.textField.delegate = self - - SigninEditingState.signinEditingStateActive = true - if UIAccessibility.isVoiceOverRunning { - // Quiet repetitive VoiceOver elements. - codeField?.placeholder = nil - } - } - - /// Configure the link cell. - /// - func configureTextLinkButton(_ cell: TextLinkButtonTableViewCell) { - cell.configureButton(text: WordPressAuthenticator.shared.displayStrings.textCodeButtonTitle, icon: .phoneIcon) - - cell.actionHandler = { [weak self] in - guard let self else { return } - - self.tracker.track(click: .sendCodeWithText) - self.requestCode() - } - } - - /// Configure the security key link cell. - /// - func configureEnterSecurityKeyLinkButton(_ cell: TextLinkButtonTableViewCell) { - cell.configureButton(text: WordPressAuthenticator.shared.displayStrings.securityKeyButtonTitle, - icon: .keyIcon, - accessibilityIdentifier: TextLinkButtonTableViewCell.Constants.passkeysID) - - cell.actionHandler = { [weak self] in - guard let self else { return } - - self.tracker.track(click: .enterSecurityKey) - self.loginWithSecurityKeys() - } - } - - /// Configure the error message cell. - /// - func configureErrorLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: errorMessage, style: .error) - if shouldChangeVoiceOverFocus { - UIAccessibility.post(notification: .layoutChanged, argument: cell) - } - } - - /// Configure the spacer cell. - /// - func configureSpacerCell(_ cell: SpacerTableViewCell, spacing: CGFloat) { - cell.spacing = spacing - } - - /// Configure the view for an editing state. - /// - func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - codeField?.becomeFirstResponder() - } - } - - /// Sets up accessibility elements in the order which they should be read aloud - /// and chooses which element to focus on at the beginning. - /// - func configureForAccessibility() { - view.accessibilityElements = [ - codeField as Any, - tableView as Any, - submitButton as Any - ] - - UIAccessibility.post(notification: .screenChanged, argument: codeField) - } - - /// Rows listed in the order they were created. - /// - enum Row: Equatable { - case instructions - case code - case alternateInstructions - case sendCode - case enterSecurityKey - case errorMessage - case spacer(CGFloat) - - var reuseIdentifier: String { - switch self { - case .instructions: - return TextLabelTableViewCell.reuseIdentifier - case .code: - return TextFieldTableViewCell.reuseIdentifier - case .alternateInstructions: - return TextLabelTableViewCell.reuseIdentifier - case .sendCode: - return TextLinkButtonTableViewCell.reuseIdentifier - case .enterSecurityKey: - return TextLinkButtonTableViewCell.reuseIdentifier - case .errorMessage: - return TextLabelTableViewCell.reuseIdentifier - case .spacer: - return SpacerTableViewCell.reuseIdentifier - } - } - } - - enum LocalizedText { - static let bad2FAMessage = NSLocalizedString("Whoops, that's not a valid two-factor verification code. Double-check your code and try again!", comment: "Error message shown when an incorrect two factor code is provided.") - static let numericalCode = NSLocalizedString("A verification code will only contain numbers.", comment: "Shown when a user types a non-number into the two factor field.") - static let invalidCode = NSLocalizedString("That doesn't appear to be a valid verification code.", comment: "Shown when a user pastes a code into the two factor field that contains letters or is the wrong length") - static let smsSent = NSLocalizedString("SMS Sent", comment: "One Time Code has been sent via SMS") - static let invalidKey = NSLocalizedString("Whoops, that security key does not seem valid. Please try again with another one", - comment: "Error when the uses chooses an invalid security key on the 2FA screen.") - static let timeoutError = NSLocalizedString("Time's up, but don't worry, your security is our priority. Please try again!", - comment: "Error when the uses takes more than 1 minute to submit a security key.") - static let unknownError = NSLocalizedString("Whoops, something went wrong. Please try again!", comment: "Generic error on the 2FA screen") - } -} - -private extension TwoFAViewController { - /// Simple spacer cell for a table view. - /// - final class SpacerTableViewCell: UITableViewCell { - - /// Static identifier - /// - static let reuseIdentifier = "SpacerTableViewCell" - - /// Gets or sets the desired vertical spacing. - /// - var spacing: CGFloat { - get { - heightConstraint.constant - } - set { - heightConstraint.constant = newValue - } - } - - /// Determines the view height internally - /// - private let heightConstraint: NSLayoutConstraint - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - - let spacerView = UIView() - spacerView.translatesAutoresizingMaskIntoConstraints = false - heightConstraint = spacerView.heightAnchor.constraint(equalToConstant: 0) - - super.init(style: style, reuseIdentifier: reuseIdentifier) - - addSubview(spacerView) - NSLayoutConstraint.activate([ - spacerView.topAnchor.constraint(equalTo: topAnchor), - spacerView.bottomAnchor.constraint(equalTo: bottomAnchor), - spacerView.leadingAnchor.constraint(equalTo: leadingAnchor), - spacerView.trailingAnchor.constraint(equalTo: trailingAnchor), - heightConstraint - ]) - } - - required init?(coder: NSCoder) { - fatalError("Not implemented") - } - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/GetStarted/GetStarted.storyboard b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/GetStarted/GetStarted.storyboard deleted file mode 100644 index 36179af4040c..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/GetStarted/GetStarted.storyboard +++ /dev/null @@ -1,159 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/GetStarted/GetStartedViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/GetStarted/GetStartedViewController.swift deleted file mode 100644 index 368d120bb14a..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/GetStarted/GetStartedViewController.swift +++ /dev/null @@ -1,987 +0,0 @@ -import UIKit -import SafariServices -import WordPressKit -import WordPressShared - -/// The source for the sign in flow for external tracking. -public enum SignInSource: Equatable { - /// Initiated from the WP.com login CTA. - case wpCom - /// Initiated from the WP.com login flow that starts with site address. - case wpComSiteAddress - /// Other source identifier from the host app. - case custom(source: String) -} - -/// The error during the sign in flow. -public enum SignInError: Error { - case invalidWPComEmail(source: SignInSource) - case invalidWPComPassword(source: SignInSource) - - init?(error: Error, source: SignInSource?) { - // `WordPressComRestApi` currently may return an WordPressComRestApiEndpointError, but it will later be changed - // to return `WordPressAPIError`. We'll handle both cases for now. - var restApiError = error as? WordPressComRestApiEndpointError - - if restApiError == nil, - let apiError = error as? WordPressAPIError, - case let .endpointError(endpointError) = apiError { - restApiError = endpointError - } - - if let restApiError, restApiError.code == .unknown { - if let source, restApiError.apiErrorCode == "unknown_user" { - self = .invalidWPComEmail(source: source) - } else { - return nil - } - } - - return nil - } -} - -class GetStartedViewController: LoginViewController, NUXKeyboardResponder { - - private enum ScreenMode { - /// For signing in using .org site credentials - /// - case signInUsingSiteCredentials - - /// For signing in using WPCOM credentials or social accounts - case signInUsingWordPressComOrSocialAccounts - } - - // MARK: - NUXKeyboardResponder constraints - @IBOutlet var bottomContentConstraint: NSLayoutConstraint? - - // Required for `NUXKeyboardResponder` but unused here. - var verticalCenterConstraint: NSLayoutConstraint? - - // MARK: - Properties - @IBOutlet private weak var tableView: UITableView! - @IBOutlet private weak var leadingDividerLine: UIView! - @IBOutlet private weak var leadingDividerLineWidth: NSLayoutConstraint! - @IBOutlet private weak var dividerStackView: UIStackView! - @IBOutlet private weak var dividerLabel: UILabel! - @IBOutlet private weak var trailingDividerLine: UIView! - @IBOutlet private weak var trailingDividerLineWidth: NSLayoutConstraint! - - private weak var emailField: UITextField? - // This is to contain the password selected by password auto-fill. - // When it is populated, login is attempted. - @IBOutlet private weak var hiddenPasswordField: UITextField? - - // This is public so it can be set from StoredCredentialsAuthenticator. - var errorMessage: String? - - var source: SignInSource? { - didSet { - WordPressAuthenticator.shared.signInSource = source - } - } - - private var rows = [Row]() - private var buttonViewController: NUXButtonViewController? - private let configuration = WordPressAuthenticator.shared.configuration - private var shouldChangeVoiceOverFocus: Bool = false - - private var passwordCoordinator: PasswordCoordinator? - - private lazy var storedCredentialsAuthenticator = StoredCredentialsAuthenticator(onCancel: { [weak self] in - // Since the authenticator has its own flow - self?.tracker.resetState() - }) - - /// Sign in with site credentials button will be displayed based on the `screenMode` - /// - private var screenMode: ScreenMode { - guard configuration.enableSiteCredentialsLoginForSelfHostedSites, - loginFields.siteAddress.isEmpty == false else { - return .signInUsingWordPressComOrSocialAccounts - } - return .signInUsingSiteCredentials - } - - // Submit button displayed in the table footer. - private lazy var continueButton: NUXButton = { - let button = NUXButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.isPrimary = true - button.isEnabled = false - button.addTarget(self, action: #selector(handleSubmitButtonTapped), for: .touchUpInside) - button.accessibilityIdentifier = ButtonConfiguration.Continue.accessibilityIdentifier - button.setTitle(ButtonConfiguration.Continue.title, for: .normal) - - return button - }() - - // "What is WordPress.com?" button - private lazy var whatisWPCOMButton: UIButton = { - let button = UIButton() - button.setTitle(WordPressAuthenticator.shared.displayStrings.whatIsWPComLinkTitle, for: .normal) - let buttonTitleColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonColor ?? WordPressAuthenticator.shared.style.textButtonColor - let buttonHighlightColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonHighlightColor ?? WordPressAuthenticator.shared.style.textButtonHighlightColor - button.titleLabel?.font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - button.setTitleColor(buttonTitleColor, for: .normal) - button.setTitleColor(buttonHighlightColor, for: .highlighted) - button.addTarget(self, action: #selector(whatIsWPComButtonTapped(_:)), for: .touchUpInside) - return button - }() - - private var showsContinueButtonAtTheBottom: Bool { - configuration.enableSocialLogin == false - } - - override open var sourceTag: WordPressSupportSourceTag { - get { - return .loginEmail - } - } - - // MARK: - View - - override func viewDidLoad() { - super.viewDidLoad() - - configureNavBar() - setupTable() - registerTableViewCells() - loadRows() - setupTableFooterView() - configureDivider() - - if screenMode == .signInUsingSiteCredentials { - configureButtonViewControllerForSiteCredentialsMode() - } else if configuration.enableSocialLogin == false { - configureButtonViewControllerWithoutSocialLogin() - } else { - configureSocialButtons() - } - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - refreshEmailField() - - // Ensure the continue button matches the validity of the email field - configureContinueButton(animating: false) - - if errorMessage != nil { - shouldChangeVoiceOverFocus = true - } - } - - override open func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - configureAnalyticsTracker() - - errorMessage = nil - hiddenPasswordField?.text = nil - hiddenPasswordField?.isAccessibilityElement = false - - if showsContinueButtonAtTheBottom { - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - } - - if screenMode == .signInUsingWordPressComOrSocialAccounts && isMovingToParent { - showiCloudKeychainLoginFlow() - } - } - - /// Starts the iCloud Keychain login flow if the conditions are given. - /// - private func showiCloudKeychainLoginFlow() { - guard WordPressAuthenticator.shared.configuration.enableUnifiedAuth, - let navigationController else { - return - } - - storedCredentialsAuthenticator.showPicker(from: navigationController) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - tableView.updateFooterHeight() - } - - // MARK: - Overrides - - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? - WordPressAuthenticator.shared.style.statusBarStyle - } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - super.prepare(for: segue, sender: sender) - - if let vc = segue.destination as? NUXButtonViewController { - buttonViewController = vc - } - } - - override func configureViewLoading(_ loading: Bool) { - configureContinueButton(animating: loading) - navigationItem.hidesBackButton = loading - } - - override func enableSubmit(animating: Bool) -> Bool { - return !animating && canSubmit() - } - - private func refreshEmailField() { - // It's possible that the password screen could have changed the loginFields username, for example when using - // autofill from a password manager. Let's ensure the loginFields matches the email field. - loginFields.username = emailField?.nonNilTrimmedText() ?? loginFields.username - } -} - -// MARK: - UITableViewDataSource - -extension GetStartedViewController: UITableViewDataSource { - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return rows.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - return cell - } -} - -// MARK: - Private methods - -private extension GetStartedViewController { - - // MARK: - Configuration - - func configureNavBar() { - navigationItem.title = WordPressAuthenticator.shared.displayStrings.getStartedTitle - } - - func setupTable() { - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - } - - func setupTableFooterView() { - let stackView = UIStackView() - stackView.axis = .vertical - stackView.alignment = .fill - stackView.spacing = Constants.FooterStackView.spacing - stackView.layoutMargins = Constants.FooterStackView.layoutMargins - stackView.isLayoutMarginsRelativeArrangement = true - - if showsContinueButtonAtTheBottom == false { - // Continue button will be added to `buttonViewController` along with sign in with site credentials button when `screenMode` is `signInUsingSiteCredentials` - // and simplified login flow is disabled. - stackView.addArrangedSubview(continueButton) - } - - if configuration.whatIsWPComURL != nil { - let stackViewWithCenterAlignment = UIStackView() - stackViewWithCenterAlignment.axis = .vertical - stackViewWithCenterAlignment.alignment = .center - - stackViewWithCenterAlignment.addArrangedSubview(whatisWPCOMButton) - - stackView.addArrangedSubview(stackViewWithCenterAlignment) - } - - tableView.tableFooterView = stackView - tableView.updateFooterHeight() - } - - /// Style the "OR" divider. - /// - func configureDivider() { - guard showsContinueButtonAtTheBottom == false else { - return dividerStackView.isHidden = true - } - let color = WordPressAuthenticator.shared.unifiedStyle?.borderColor ?? WordPressAuthenticator.shared.style.primaryNormalBorderColor - leadingDividerLine.backgroundColor = color - leadingDividerLineWidth.constant = WPStyleGuide.hairlineBorderWidth - trailingDividerLine.backgroundColor = color - trailingDividerLineWidth.constant = WPStyleGuide.hairlineBorderWidth - dividerLabel.textColor = color - dividerLabel.text = NSLocalizedString("Or", comment: "Divider on initial auth view separating auth options.").localizedUppercase - } - - // MARK: - Continue Button Action - - @objc func handleSubmitButtonTapped() { - tracker.track(click: .submit) - validateForm() - } - - // MARK: - Sign in with site credentials Button Action - @objc func handleSiteCredentialsButtonTapped() { - tracker.track(click: .signInWithSiteCredentials) - goToSiteCredentialsScreen() - } - - // MARK: - What is WordPress.com Button Action - - @IBAction func whatIsWPComButtonTapped(_ sender: UIButton) { - tracker.track(click: .whatIsWPCom) - guard let whatIsWPCom = configuration.whatIsWPComURL else { - return - } - UIApplication.shared.open(whatIsWPCom) - } - - // MARK: - Hidden Password Field Action - - @IBAction func handlePasswordFieldDidChange(_ sender: UITextField) { - attemptAutofillLogin() - } - - // MARK: - Table Management - - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib(), - TextFieldTableViewCell.reuseIdentifier: TextFieldTableViewCell.loadNib(), - TextWithLinkTableViewCell.reuseIdentifier: TextWithLinkTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - } - - /// Describes how the tableView rows should be rendered. - /// - func loadRows() { - rows = [.instructions, .email] - - if let authenticationDelegate = WordPressAuthenticator.shared.delegate, authenticationDelegate.wpcomTermsOfServiceEnabled { - rows.append(.tos) - } - - if let errorText = errorMessage, !errorText.isEmpty { - rows.append(.errorMessage) - } - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextFieldTableViewCell: - configureEmailField(cell) - case let cell as TextWithLinkTableViewCell: - configureTextWithLink(cell) - case let cell as TextLabelTableViewCell where row == .errorMessage: - configureErrorLabel(cell) - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the instruction cell. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.getStartedInstructions) - } - - /// Configure the email cell. - /// - func configureEmailField(_ cell: TextFieldTableViewCell) { - cell.configure(withStyle: .email, - placeholder: WordPressAuthenticator.shared.displayStrings.emailAddressPlaceholder, - text: loginFields.username) - cell.textField.delegate = self - emailField = cell.textField - - cell.onChangeSelectionHandler = { [weak self] textfield in - self?.loginFields.username = textfield.nonNilTrimmedText() - self?.configureContinueButton(animating: false) - } - - if UIAccessibility.isVoiceOverRunning { - // Quiet repetitive elements in VoiceOver. - emailField?.placeholder = nil - } - } - - /// Configure the link cell. - /// - func configureTextWithLink(_ cell: TextWithLinkTableViewCell) { - cell.configureButton(markedText: WordPressAuthenticator.shared.displayStrings.loginTermsOfService) - - cell.actionHandler = { [weak self] in - self?.termsTapped() - } - } - - /// Configure the error message cell. - /// - func configureErrorLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: errorMessage, style: .error) - - if shouldChangeVoiceOverFocus { - UIAccessibility.post(notification: .layoutChanged, argument: cell) - } - } - - /// Rows listed in the order they were created. - /// - enum Row { - case instructions - case email - case tos - case errorMessage - - var reuseIdentifier: String { - switch self { - case .instructions, .errorMessage: - return TextLabelTableViewCell.reuseIdentifier - case .email: - return TextFieldTableViewCell.reuseIdentifier - case .tos: - return TextWithLinkTableViewCell.reuseIdentifier - } - } - } - - enum Constants { - enum FooterStackView { - static let spacing = 16.0 - static let layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 0, right: 16) - } - } - - // MARK: Analytics - // - func configureAnalyticsTracker() { - // Configure tracker flow based on screen mode. - switch screenMode { - case .signInUsingSiteCredentials: - tracker.set(flow: .loginWithSiteAddress) - case .signInUsingWordPressComOrSocialAccounts: - tracker.set(flow: .wpCom) - } - - let stepValue: AuthenticatorAnalyticsTracker.Step = configuration.useEnterEmailAddressAsStepValueForGetStartedVC ? .enterEmailAddress : .start - if isMovingToParent { - tracker.track(step: stepValue) - } else { - tracker.set(step: stepValue) - } - } -} - -// MARK: - Validation - -private extension GetStartedViewController { - - /// Configures appearance of the submit button. - /// - func configureContinueButton(animating: Bool) { - if showsContinueButtonAtTheBottom { - buttonViewController?.setTopButtonState(isLoading: animating, - isEnabled: enableSubmit(animating: animating)) - } else { - continueButton.showActivityIndicator(animating) - continueButton.isEnabled = enableSubmit(animating: animating) - } - } - - /// Whether the form can be submitted. - /// - func canSubmit() -> Bool { - return EmailFormatValidator.validate(string: loginFields.username) - } - - /// Validates email address and proceeds with the submit action. - /// Empties loginFields.meta.socialService as - /// social signin does not require form validation. - /// - func validateForm() { - loginFields.meta.socialService = nil - displayError(message: "") - - guard EmailFormatValidator.validate(string: loginFields.username) else { - present(buildInvalidEmailAlertGeneric(), animated: true, completion: nil) - return - } - - configureViewLoading(true) - let service = WordPressComAccountService() - service.isPasswordlessAccount(username: loginFields.username, - success: { [weak self] passwordless in - self?.configureViewLoading(false) - self?.loginFields.meta.passwordless = passwordless - passwordless ? self?.requestAuthenticationLink() : self?.showPasswordOrMagicLinkView() - }, - failure: { [weak self] error in - WordPressAuthenticator.track(.loginFailed, error: error) - WPLogError(error.localizedDescription) - guard let self else { - return - } - self.configureViewLoading(false) - - self.handleLoginError(error) - }) - } - - /// Show the Password entry view. - /// - func showPasswordView() { - guard let vc = PasswordViewController.instantiate(from: .password) else { - WPLogError("Failed to navigate to PasswordViewController from GetStartedViewController") - return - } - - vc.source = source - vc.loginFields = loginFields - vc.trackAsPasswordChallenge = false - - navigationController?.pushViewController(vc, animated: true) - } - - /// Show the password or magic link view based on the configuration. - /// - func showPasswordOrMagicLinkView() { - guard let navigationController else { - return - } - configureViewLoading(true) - let coordinator = PasswordCoordinator(navigationController: navigationController, - source: source, - loginFields: loginFields, - tracker: tracker, - configuration: configuration) - passwordCoordinator = coordinator - Task { @MainActor [weak self] in - guard let self else { return } - await coordinator.start() - self.configureViewLoading(false) - } - } - - /// Handle errors when attempting to log in with an email address - /// - func handleLoginError(_ error: Error) { - let userInfo = (error as NSError).userInfo - let errorCode = userInfo[WordPressComRestApi.ErrorKeyErrorCode] as? String - - if configuration.enableSignUp, errorCode == "unknown_user" { - self.sendEmail() - } else if errorCode == "email_login_not_allowed" { - // If we get this error, we know we have a WordPress.com user but their - // email address is flagged as suspicious. They need to login via their - // username instead. - self.showSelfHostedWithError(error) - } else { - let signInError = SignInError(error: error, source: source) ?? error - guard let authenticationDelegate = WordPressAuthenticator.shared.delegate, - authenticationDelegate.shouldHandleError(signInError) else { - displayError(error, sourceTag: sourceTag) - return - } - - /// Hand over control to the host app. - authenticationDelegate.handleError(signInError) { customUI in - // Setting the rightBarButtonItems of the custom UI before pushing the view controller - // and resetting the navigationController's navigationItem after the push seems to be the - // only combination that gets the Help button to show up. - customUI.navigationItem.rightBarButtonItems = self.navigationItem.rightBarButtonItems - self.navigationController?.navigationItem.rightBarButtonItems = self.navigationItem.rightBarButtonItems - - self.navigationController?.pushViewController(customUI, animated: true) - } - } - } - - // MARK: - Send email - - /// Makes the call to request a magic signup link be emailed to the user. - /// - private func sendEmail() { - tracker.set(flow: .signup) - loginFields.meta.emailMagicLinkSource = .signup - - configureSubmitButton(animating: true) - - let service = WordPressComAccountService() - service.requestSignupLink(for: loginFields.username, - success: { [weak self] in - self?.didRequestSignupLink() - self?.configureSubmitButton(animating: false) - }, failure: { [weak self] (error: Error) in - WPLogError("Request for signup link email failed.") - - guard let self else { - return - } - - self.tracker.track(failure: error.localizedDescription) - self.displayError(error, sourceTag: self.sourceTag) - self.configureSubmitButton(animating: false) - }) - } - - private func didRequestSignupLink() { - guard let vc = SignupMagicLinkViewController.instantiate(from: .unifiedSignup) else { - WPLogError("Failed to navigate from UnifiedSignupViewController to SignupMagicLinkViewController") - return - } - - vc.loginFields = loginFields - vc.loginFields.restrictToWPCom = true - - navigationController?.pushViewController(vc, animated: true) - } - - /// Makes the call to request a magic authentication link be emailed to the user. - /// - func requestAuthenticationLink() { - loginFields.meta.emailMagicLinkSource = .login - - let email = loginFields.username - guard email.isValidEmail() else { - present(buildInvalidEmailLinkAlert(), animated: true, completion: nil) - return - } - - configureViewLoading(true) - let service = WordPressComAccountService() - service.requestAuthenticationLink(for: email, - jetpackLogin: loginFields.meta.jetpackLogin, - success: { [weak self] in - self?.didRequestAuthenticationLink() - self?.configureViewLoading(false) - }, failure: { [weak self] (error: Error) in - guard let self else { - return - } - - self.tracker.track(failure: error.localizedDescription) - - self.displayError(error, sourceTag: self.sourceTag) - self.configureViewLoading(false) - }) - } - - /// When a magic link successfully sends, navigate the user to the next step. - /// - func didRequestAuthenticationLink() { - guard let vc = LoginMagicLinkViewController.instantiate(from: .unifiedLoginMagicLink) else { - WPLogError("Failed to navigate to LoginMagicLinkViewController from GetStartedViewController") - return - } - - vc.loginFields = self.loginFields - vc.loginFields.restrictToWPCom = true - navigationController?.pushViewController(vc, animated: true) - } - - /// Build the alert message when the email address is invalid - /// - private func buildInvalidEmailAlertGeneric() -> UIAlertController { - let title = NSLocalizedString("Invalid Email Address", - comment: "Title of an alert letting the user know the email address that they've entered isn't valid") - let message = NSLocalizedString("Please enter a valid email address for a WordPress.com account.", - comment: "An error message.") - - return buildInvalidEmailAlert(title: title, message: message) - } - - /// Build the alert message when the email address is invalid so a link cannot be requested - /// - private func buildInvalidEmailLinkAlert() -> UIAlertController { - let title = NSLocalizedString("Can Not Request Link", - comment: "Title of an alert letting the user know") - let message = NSLocalizedString("A valid email address is needed to mail an authentication link. Please return to the previous screen and provide a valid email address.", - comment: "An error message.") - - return buildInvalidEmailAlert(title: title, message: message) - } - - private func buildInvalidEmailAlert(title: String, message: String) -> UIAlertController { - - let helpActionTitle = NSLocalizedString("Need help?", - comment: "Takes the user to get help") - let okActionTitle = NSLocalizedString("OK", - comment: "Dismisses the alert") - let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - - alert.addActionWithTitle(helpActionTitle, - style: .cancel, - handler: { _ in - WordPressAuthenticator.shared.delegate?.presentSupportRequest(from: self, sourceTag: .loginEmail) - }) - - alert.addActionWithTitle(okActionTitle, style: .default, handler: nil) - - return alert - } - - /// When password autofill has entered a password on this screen, attempt to login immediately - /// - func attemptAutofillLogin() { - // Even though there was no explicit submit action by the user, we'll interpret - // the credentials selection as such. - tracker.track(click: .submit) - - loginFields.password = hiddenPasswordField?.text ?? "" - loginFields.meta.socialService = nil - displayError(message: "") - validateFormAndLogin() - } - - /// Configures loginFields to log into wordpress.com and navigates to the selfhosted username/password form. - /// Displays the specified error message when the new view controller appears. - /// - func showSelfHostedWithError(_ error: Error) { - loginFields.siteAddress = "https://wordpress.com" - errorToPresent = error - - tracker.track(failure: error.localizedDescription) - - guard let vc = SiteCredentialsViewController.instantiate(from: .siteAddress) else { - WPLogError("Failed to navigate to SiteCredentialsViewController from GetStartedViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - /// Navigates to site credentials screen where .org site credentials can be entered - /// - func goToSiteCredentialsScreen() { - guard let vc = SiteCredentialsViewController.instantiate(from: .siteAddress) else { - WPLogError("Failed to navigate from GetStartedViewController to SiteCredentialsViewController") - return - } - - vc.loginFields = loginFields.copy() - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } -} - -// MARK: - Social Button Management - -private extension GetStartedViewController { - - func configureSocialButtons() { - guard let buttonViewController else { - return - } - - buttonViewController.hideShadowView() - - if configuration.enableSignInWithApple { - buttonViewController.setupTopButtonFor(socialService: .apple) { [weak self] in - self?.appleTapped() - } - } - - buttonViewController.setupButtomButtonFor(socialService: .google) { [weak self] in - self?.googleTapped() - } - - let termsButton = WPStyleGuide.signupTermsButton() - buttonViewController.stackView?.addArrangedSubview(termsButton) - termsButton.addTarget(self, action: #selector(termsTapped), for: .touchUpInside) - } - - func configureButtonViewControllerForSiteCredentialsMode() { - guard let buttonViewController else { - return - } - - buttonViewController.hideShadowView() - - if configuration.enableSocialLogin { - configureSocialButtons() - - // Setup Sign in with site credentials button - buttonViewController.setupTertiaryButton(attributedTitle: WPStyleGuide.formattedSignInWithSiteCredentialsString(), - isPrimary: false, - accessibilityIdentifier: ButtonConfiguration.SignInWithSiteCredentials.accessibilityIdentifier) { [weak self] in - self?.handleSiteCredentialsButtonTapped() - } - } else { - // Add a "Continue" button here as the `continueButton` at the top will be hidden - // - if showsContinueButtonAtTheBottom { - buttonViewController.setupTopButton(title: ButtonConfiguration.Continue.title, - isPrimary: true, - accessibilityIdentifier: ButtonConfiguration.Continue.accessibilityIdentifier) { [weak self] in - self?.handleSubmitButtonTapped() - } - } - - // Setup Sign in with site credentials button - buttonViewController.setupBottomButton(attributedTitle: WPStyleGuide.formattedSignInWithSiteCredentialsString(), - isPrimary: false, - accessibilityIdentifier: ButtonConfiguration.SignInWithSiteCredentials.accessibilityIdentifier) { [weak self] in - self?.handleSiteCredentialsButtonTapped() - } - } - } - - func configureButtonViewControllerWithoutSocialLogin() { - guard let buttonViewController else { - return - } - - buttonViewController.hideShadowView() - - if showsContinueButtonAtTheBottom { - // Add a "Continue" button here as the `continueButton` at the top will be hidden - // - buttonViewController.setupTopButton(title: ButtonConfiguration.Continue.title, - isPrimary: true, - accessibilityIdentifier: ButtonConfiguration.Continue.accessibilityIdentifier) { [weak self] in - self?.handleSubmitButtonTapped() - } - } - } - - @objc func appleTapped() { - tracker.track(click: .loginWithApple) - - AppleAuthenticator.sharedInstance.delegate = self - AppleAuthenticator.sharedInstance.showFrom(viewController: self) - } - - @objc func googleTapped() { - tracker.track(click: .loginWithGoogle) - - guard let toVC = GoogleAuthViewController.instantiate(from: .googleAuth) else { - WPLogError("Failed to navigate to GoogleAuthViewController from GetStartedViewController") - return - } - - navigationController?.pushViewController(toVC, animated: true) - } - - @objc func termsTapped() { - tracker.track(click: .termsOfService) - - UIApplication.shared.open(configuration.wpcomTermsOfServiceURL) - } -} - -// MARK: - SFSafariViewControllerDelegate - -extension GetStartedViewController: SFSafariViewControllerDelegate { - func safariViewControllerDidFinish(_ controller: SFSafariViewController) { - // This will only work when the user taps "Done" in the terms of service screen. - // It won't be executed if the user dismisses the terms of service VC by sliding it out of view. - // Unfortunately I haven't found a way to track that scenario. - // - tracker.track(click: .dismiss) - } -} - -// MARK: - AppleAuthenticatorDelegate - -extension GetStartedViewController: AppleAuthenticatorDelegate { - - func showWPComLogin(loginFields: LoginFields) { - self.loginFields = loginFields - showPasswordView() - } - - func showApple2FA(loginFields: LoginFields) { - self.loginFields = loginFields - signInAppleAccount() - } - - func authFailedWithError(message: String) { - displayErrorAlert(message, sourceTag: .loginApple) - tracker.set(flow: .wpCom) - } -} - -// MARK: - LoginFacadeDelegate - -extension GetStartedViewController { - - // Used by SIWA when logging with with a passwordless, 2FA account. - // - func needsMultifactorCode(forUserID userID: Int, andNonceInfo nonceInfo: SocialLogin2FANonceInfo) { - configureViewLoading(false) - socialNeedsMultifactorCode(forUserID: userID, andNonceInfo: nonceInfo) - } -} - -// MARK: - UITextFieldDelegate - -extension GetStartedViewController: UITextFieldDelegate { - - func textFieldDidBeginEditing(_ textField: UITextField) { - tracker.track(click: .selectEmailField) - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if canSubmit() { - validateForm() - } - return true - } -} - -// MARK: - Keyboard Notifications - -extension GetStartedViewController { - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } -} - -// MARK: - Button configuration - -private extension GetStartedViewController { - enum ButtonConfiguration { - enum Continue { - static let title = WordPressAuthenticator.shared.displayStrings.continueButtonTitle - static let accessibilityIdentifier = "Get Started Email Continue Button" - } - - enum SignInWithSiteCredentials { - static let accessibilityIdentifier = "Sign in with site credentials Button" - } - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleAuth.storyboard b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleAuth.storyboard deleted file mode 100644 index 38789068fba7..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleAuth.storyboard +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleAuthViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleAuthViewController.swift deleted file mode 100644 index fa380cfce74a..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleAuthViewController.swift +++ /dev/null @@ -1,165 +0,0 @@ -import UIKit -import SVProgressHUD -import WordPressShared - -/// View controller that handles the google authentication flow -/// -class GoogleAuthViewController: LoginViewController { - - // MARK: - Properties - - private var hasShownGoogle = false - @IBOutlet var titleLabel: UILabel? - - override var sourceTag: WordPressSupportSourceTag { - get { - return .wpComAuthWaitingForGoogle - } - } - - // MARK: - View - - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.waitingForGoogleTitle - - titleLabel?.text = NSLocalizedString("Waiting for Google to complete…", comment: "Message shown on screen while waiting for Google to finish its signup process.") - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - showGoogleScreenIfNeeded() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - if isMovingFromParent { - AuthenticatorAnalyticsTracker.shared.track(click: .dismiss) - } - } - - // MARK: - Overrides - - /// Style individual ViewController backgrounds, for now. - /// - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } -} - -// MARK: - Private Methods - -private extension GoogleAuthViewController { - - func showGoogleScreenIfNeeded() { - guard !hasShownGoogle else { - return - } - - // Flag this as a social sign in. - loginFields.meta.socialService = .google - - GoogleAuthenticator.sharedInstance.delegate = self - GoogleAuthenticator.sharedInstance.showFrom(viewController: self, loginFields: loginFields) - hasShownGoogle = true - } - - func showLoginErrorView(errorTitle: String, errorDescription: String) { - let socialErrorVC = LoginSocialErrorViewController(title: errorTitle, description: errorDescription) - let socialErrorNav = LoginNavigationController(rootViewController: socialErrorVC) - socialErrorVC.delegate = self - socialErrorVC.loginFields = loginFields - socialErrorVC.modalPresentationStyle = .fullScreen - present(socialErrorNav, animated: true) - } - - func showSignupConfirmationView() { - guard let vc = GoogleSignupConfirmationViewController.instantiate(from: .googleSignupConfirmation) else { - WPLogError("Failed to navigate from GoogleAuthViewController to GoogleSignupConfirmationViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } -} - -// MARK: - GoogleAuthenticatorDelegate - -extension GoogleAuthViewController: GoogleAuthenticatorDelegate { - - // MARK: - Login - - func googleFinishedLogin(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - self.loginFields = loginFields - syncWPComAndPresentEpilogue(credentials: credentials) - } - - func googleNeedsMultifactorCode(loginFields: LoginFields) { - self.loginFields = loginFields - - guard let vc = TwoFAViewController.instantiate(from: .twoFA) else { - WPLogError("Failed to navigate from GoogleAuthViewController to TwoFAViewController") - return - } - - vc.loginFields = loginFields - navigationController?.pushViewController(vc, animated: true) - } - - func googleExistingUserNeedsConnection(loginFields: LoginFields) { - self.loginFields = loginFields - - guard let vc = PasswordViewController.instantiate(from: .password) else { - WPLogError("Failed to navigate from GoogleAuthViewController to PasswordViewController") - return - } - - vc.loginFields = loginFields - navigationController?.pushViewController(vc, animated: true) - } - - func googleLoginFailed(errorTitle: String, errorDescription: String, loginFields: LoginFields, unknownUser: Bool) { - self.loginFields = loginFields - - // If login failed because there is no existing account, redirect to signup. - // Otherwise, display the error. - let redirectToSignup = unknownUser && WordPressAuthenticator.shared.configuration.enableSignupWithGoogle - - redirectToSignup ? showSignupConfirmationView() : - showLoginErrorView(errorTitle: errorTitle, errorDescription: errorDescription) - } - - func googleAuthCancelled() { - SVProgressHUD.dismiss() - navigationController?.popViewController(animated: true) - } - - // MARK: - Signup - - func googleFinishedSignup(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - // Here for protocol compliance. - } - - func googleLoggedInInstead(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - // Here for protocol compliance. - } - - func googleSignupFailed(error: Error, loginFields: LoginFields) { - // Here for protocol compliance. - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleSignupConfirmation.storyboard b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleSignupConfirmation.storyboard deleted file mode 100644 index ee7ca90ffd8f..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleSignupConfirmation.storyboard +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleSignupConfirmationViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleSignupConfirmationViewController.swift deleted file mode 100644 index b05814fe81d5..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleSignupConfirmationViewController.swift +++ /dev/null @@ -1,256 +0,0 @@ -import UIKit -import WordPressShared - -class GoogleSignupConfirmationViewController: LoginViewController { - - // MARK: - Properties - - @IBOutlet private weak var tableView: UITableView! - private var rows = [Row]() - private var errorMessage: String? - private var shouldChangeVoiceOverFocus: Bool = false - - override var sourceTag: WordPressSupportSourceTag { - get { - return .wpComAuthGoogleSignupConfirmation - } - } - - // MARK: - View - - override func viewDidLoad() { - super.viewDidLoad() - - removeGoogleWaitingView() - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.signUpTitle - - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - - localizePrimaryButton() - registerTableViewCells() - loadRows() - configureForAccessibility() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - tracker.set(flow: .signupWithGoogle) - - if isBeingPresentedInAnyWay { - tracker.track(step: .start) - } else { - tracker.set(step: .start) - } - } - - // MARK: - Overrides - - /// Style individual ViewController backgrounds, for now. - /// - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - /// Style individual ViewController status bars. - /// - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } - - /// Override the title on 'submit' button - /// - override func localizePrimaryButton() { - submitButton?.setTitle(WordPressAuthenticator.shared.displayStrings.createAccountButtonTitle, for: .normal) - } - - override func displayError(message: String, moveVoiceOverFocus: Bool = false) { - if errorMessage != message { - errorMessage = message - shouldChangeVoiceOverFocus = moveVoiceOverFocus - loadRows() - tableView.reloadData() - } - } -} - -// MARK: - UITableViewDataSource - -extension GoogleSignupConfirmationViewController: UITableViewDataSource { - - /// Returns the number of rows in a section. - /// - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return rows.count - } - - /// Configure cells delegate method. - /// - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - - return cell - } -} - -// MARK: - Private Extension - -private extension GoogleSignupConfirmationViewController { - - // MARK: - Button Handling - - @IBAction func handleSubmit() { - tracker.track(click: .submit) - tracker.track(click: .createAccount) - - configureSubmitButton(animating: true) - GoogleAuthenticator.sharedInstance.delegate = self - GoogleAuthenticator.sharedInstance.createGoogleAccount(loginFields: loginFields) - } - - // MARK: - Table Management - - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - GravatarEmailTableViewCell.reuseIdentifier: GravatarEmailTableViewCell.loadNib(), - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - } - - /// Describes how the tableView rows should be rendered. - /// - func loadRows() { - rows = [.gravatarEmail, .instructions] - - if let errorText = errorMessage, !errorText.isEmpty { - rows.append(.errorMessage) - } - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as GravatarEmailTableViewCell: - configureGravatarEmail(cell) - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextLabelTableViewCell where row == .errorMessage: - configureErrorLabel(cell) - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the gravatar + email cell. - /// - func configureGravatarEmail(_ cell: GravatarEmailTableViewCell) { - cell.configure(withEmail: loginFields.username) - } - - /// Configure the instruction cell. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.googleSignupInstructions, style: .body) - } - - /// Configure the error message cell. - /// - func configureErrorLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: errorMessage, style: .error) - if shouldChangeVoiceOverFocus { - UIAccessibility.post(notification: .layoutChanged, argument: cell) - } - } - - /// Sets up accessibility elements in the order which they should be read aloud - /// and chooses which element to focus on at the beginning. - /// - func configureForAccessibility() { - view.accessibilityElements = [ - tableView as Any, - submitButton as Any - ] - - UIAccessibility.post(notification: .screenChanged, argument: tableView) - } - - // MARK: - Private Constants - - /// Rows listed in the order they were created. - /// - enum Row { - case gravatarEmail - case instructions - case errorMessage - - var reuseIdentifier: String { - switch self { - case .gravatarEmail: - return GravatarEmailTableViewCell.reuseIdentifier - case .instructions, .errorMessage: - return TextLabelTableViewCell.reuseIdentifier - } - } - } -} - -// MARK: - GoogleAuthenticatorDelegate - -extension GoogleSignupConfirmationViewController: GoogleAuthenticatorDelegate { - - // MARK: - Signup - - func googleFinishedSignup(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - self.loginFields = loginFields - showSignupEpilogue(for: credentials) - } - - func googleLoggedInInstead(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - self.loginFields = loginFields - showLoginEpilogue(for: credentials) - } - - func googleSignupFailed(error: Error, loginFields: LoginFields) { - configureSubmitButton(animating: false) - self.loginFields = loginFields - displayError(message: error.localizedDescription, moveVoiceOverFocus: true) - } - - // MARK: - Login - - func googleFinishedLogin(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - // Here for protocol compliance. - } - - func googleNeedsMultifactorCode(loginFields: LoginFields) { - // Here for protocol compliance. - } - - func googleExistingUserNeedsConnection(loginFields: LoginFields) { - // Here for protocol compliance. - } - - func googleLoginFailed(errorTitle: String, errorDescription: String, loginFields: LoginFields, unknownUser: Bool) { - // Here for protocol compliance. - } - - func googleAuthCancelled() { - // Here for protocol compliance. - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/LoginMagicLink.storyboard b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/LoginMagicLink.storyboard deleted file mode 100644 index 9e7fdb1fb5ae..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/LoginMagicLink.storyboard +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/LoginMagicLinkViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/LoginMagicLinkViewController.swift deleted file mode 100644 index 761eaf44fd48..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/LoginMagicLinkViewController.swift +++ /dev/null @@ -1,180 +0,0 @@ -import UIKit -import WordPressShared - -/// Unified LoginMagicLinkViewController: login to .com with a magic link -/// -final class LoginMagicLinkViewController: LoginViewController { - - // MARK: Properties - - @IBOutlet private weak var tableView: UITableView! - private var rows = [Row]() - private var errorMessage: String? - private var shouldChangeVoiceOverFocus: Bool = false - - override var sourceTag: WordPressSupportSourceTag { - get { - return .loginMagicLink - } - } - - // MARK: - Actions - @IBAction func handleContinueButtonTapped(_ sender: NUXButton) { - tracker.track(click: .openEmailClient) - tracker.track(step: .emailOpened) - - let linkMailPresenter = LinkMailPresenter(emailAddress: loginFields.username) - let appSelector = AppSelector(sourceView: sender) - linkMailPresenter.presentEmailClients(on: self, appSelector: appSelector) - } - - // MARK: - View lifecycle - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.logInTitle - - // Store default margin, and size table for the view. - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - - localizePrimaryButton() - registerTableViewCells() - loadRows() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - tracker.set(flow: .loginWithMagicLink) - - if isMovingToParent { - tracker.track(step: .magicLinkRequested) - } else { - tracker.set(step: .magicLinkRequested) - } - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(true) - } - - // MARK: - Overrides - - /// Style individual ViewController backgrounds, for now. - /// - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - /// Style individual ViewController status bars. - /// - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } - - /// Override the title on 'submit' button - /// - override func localizePrimaryButton() { - submitButton?.setTitle(WordPressAuthenticator.shared.displayStrings.openMailButtonTitle, for: .normal) - submitButton?.accessibilityIdentifier = "Open Mail Button" - } -} - -// MARK: - UITableViewDataSource -extension LoginMagicLinkViewController: UITableViewDataSource { - /// Returns the number of rows in a section. - /// - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return rows.count - } - - /// Configure cells delegate method. - /// - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - - return cell - } -} - -// MARK: - Private Methods -private extension LoginMagicLinkViewController { - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - GravatarEmailTableViewCell.reuseIdentifier: GravatarEmailTableViewCell.loadNib(), - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - } - - /// Describes how the tableView rows should be rendered. - /// - func loadRows() { - rows = [.persona, .instructions, .checkSpam] - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as GravatarEmailTableViewCell where row == .persona: - configureGravatarEmail(cell) - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextLabelTableViewCell where row == .checkSpam: - configureCheckSpamLabel(cell) - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the gravatar + email cell. - /// - func configureGravatarEmail(_ cell: GravatarEmailTableViewCell) { - cell.configure(withEmail: loginFields.username) - } - - /// Configure the instruction cell. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.openMailLoginInstructions, style: .body) - } - - /// Configure the "Check spam" cell. - /// - func configureCheckSpamLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.checkSpamInstructions, style: .body) - } - - // MARK: - Private Constants - - /// Rows listed in the order they were created. - /// - enum Row { - case persona - case instructions - case checkSpam - - var reuseIdentifier: String { - switch self { - case .persona: - return GravatarEmailTableViewCell.reuseIdentifier - case .instructions, .checkSpam: - return TextLabelTableViewCell.reuseIdentifier - } - } - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequestedViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequestedViewController.swift deleted file mode 100644 index 75aff3e9ca81..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequestedViewController.swift +++ /dev/null @@ -1,158 +0,0 @@ -import UIKit -import WordPressUI -import WordPressShared - -final class MagicLinkRequestedViewController: LoginViewController { - - // MARK: Properties - - @IBOutlet private weak var titleLabel: UILabel! - @IBOutlet private weak var subtitleLabel: UILabel! - @IBOutlet private weak var emailLabel: UILabel! - @IBOutlet private weak var cannotFindEmailLabel: UILabel! - @IBOutlet private weak var buttonContainerView: UIView! - @IBOutlet private weak var loginWithPasswordButton: UIButton! - - private let email: String - private let loginWithPassword: () -> Void - - private lazy var buttonViewController: NUXButtonViewController = .instance() - - init(email: String, loginWithPassword: @escaping () -> Void) { - self.email = email - self.loginWithPassword = loginWithPassword - super.init(nibName: "MagicLinkRequestedViewController", bundle: WordPressAuthenticator.bundle) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override var sourceTag: WordPressSupportSourceTag { - .wpComLoginMagicLinkAutoRequested - } - - // MARK: - View lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.logInTitle - - setupButtons() - setupTitleLabel() - setupSubtitleLabel() - setupEmailLabel() - setupCannotFindEmailLabel() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - tracker.set(flow: .loginWithMagicLink) - - if isBeingPresentedInAnyWay { - tracker.track(step: .magicLinkAutoRequested) - } else { - tracker.set(step: .magicLinkAutoRequested) - } - } - - // MARK: - Overrides - - /// Style individual ViewController backgrounds, for now. - /// - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - return super.styleBackground() - } - view.backgroundColor = unifiedBackgroundColor - } - - /// Style individual ViewController status bars. - /// - override var preferredStatusBarStyle: UIStatusBarStyle { - WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } -} - -private extension MagicLinkRequestedViewController { - func setupButtons() { - setupContinueMailButton() - setupLoginWithPasswordButton() - } - - /// Configures the primary button using the shared NUXButton style without a Storyboard. - func setupContinueMailButton() { - buttonViewController.setupTopButton(title: WordPressAuthenticator.shared.displayStrings.openMailButtonTitle, isPrimary: true, onTap: { [weak self] in - guard let self else { return } - guard let topButton = self.buttonViewController.topButton else { - return - } - self.openMail(sender: topButton) - }) - buttonViewController.move(to: self, into: buttonContainerView) - } - - /// Unfortunately, the plain text button style is not available in `NUXButton` as it currently supports primary or secondary. - /// The plain text button is configured manually here. - func setupLoginWithPasswordButton() { - loginWithPasswordButton.setTitle(Localization.loginWithPasswordAction, for: .normal) - loginWithPasswordButton.applyLinkButtonStyle() - loginWithPasswordButton.on(.touchUpInside) { [weak self] _ in - self?.loginWithPassword() - } - } - - func setupTitleLabel() { - titleLabel.text = Localization.title - titleLabel.font = WPStyleGuide.mediumWeightFont(forStyle: .title3) - titleLabel.textColor = WordPressAuthenticator.shared.unifiedStyle?.textColor - titleLabel.numberOfLines = 0 - } - - func setupSubtitleLabel() { - subtitleLabel.text = Localization.subtitle - subtitleLabel.font = WPStyleGuide.fontForTextStyle(.body) - subtitleLabel.textColor = WordPressAuthenticator.shared.unifiedStyle?.textColor - subtitleLabel.numberOfLines = 0 - } - - func setupEmailLabel() { - emailLabel.text = email - emailLabel.numberOfLines = 0 - emailLabel.font = WPStyleGuide.fontForTextStyle(.body, fontWeight: .bold) - emailLabel.textColor = WordPressAuthenticator.shared.unifiedStyle?.textColor - } - - func setupCannotFindEmailLabel() { - cannotFindEmailLabel.text = Localization.cannotFindMailLoginInstructions - cannotFindEmailLabel.numberOfLines = 0 - cannotFindEmailLabel.font = WPStyleGuide.fontForTextStyle(.footnote) - cannotFindEmailLabel.textColor = WordPressAuthenticator.shared.unifiedStyle?.textSubtleColor - } -} - -private extension MagicLinkRequestedViewController { - func openMail(sender: UIView) { - tracker.track(click: .openEmailClient) - tracker.track(step: .emailOpened) - - let linkMailPresenter = LinkMailPresenter(emailAddress: email) - let appSelector = AppSelector(sourceView: sender) - linkMailPresenter.presentEmailClients(on: self, appSelector: appSelector) - } -} - -private extension MagicLinkRequestedViewController { - enum Localization { - static let cannotFindMailLoginInstructions = NSLocalizedString("If you can’t find the email, please check your junk or spam email folder", - comment: "The instructions text about not being able to find the magic link email.") - static let title = NSLocalizedString("Check your email on this device!", - comment: "The title text on the magic link requested screen.") - static let subtitle = NSLocalizedString("We just sent a magic link to", - comment: "The subtitle text on the magic link requested screen followed by the email address.") - static let loginWithPasswordAction = NSLocalizedString("Use password to sign in", - comment: "The button title text for logging in with WP.com password instead of magic link.") - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequestedViewController.xib b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequestedViewController.xib deleted file mode 100644 index fdcd6f348132..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequestedViewController.xib +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequester.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequester.swift deleted file mode 100644 index efb7409a98ab..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequester.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation - -/// Encapsulates the async request for a magic link and email validation for use cases that send a magic link. -struct MagicLinkRequester { - /// Makes the call to request a magic authentication link be emailed to the user if possible. - func requestMagicLink(email: String, jetpackLogin: Bool) async -> Result { - await withCheckedContinuation { continuation in - guard email.isValidEmail() else { - return continuation.resume(returning: .failure(MagicLinkRequestError.invalidEmail)) - } - - let service = WordPressComAccountService() - service.requestAuthenticationLink(for: email, - jetpackLogin: jetpackLogin, - success: { - continuation.resume(returning: .success(())) - }, failure: { error in - continuation.resume(returning: .failure(error)) - }) - } - } -} - -extension MagicLinkRequester { - enum MagicLinkRequestError: Error { - case invalidEmail - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/Password.storyboard b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/Password.storyboard deleted file mode 100644 index 85cfde6f2711..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/Password.storyboard +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/PasswordCoordinator.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/PasswordCoordinator.swift deleted file mode 100644 index 01527194d607..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/PasswordCoordinator.swift +++ /dev/null @@ -1,71 +0,0 @@ -import UIKit -import WordPressShared - -/// Coordinates the navigation after entering WP.com username. -/// Based on the configuration, it could automatically send a magic link and proceed the magic link requested screen on success and fall back to password. -@MainActor -final class PasswordCoordinator { - private weak var navigationController: UINavigationController? - private let source: SignInSource? - private let loginFields: LoginFields - private let tracker: AuthenticatorAnalyticsTracker - private let configuration: WordPressAuthenticatorConfiguration - - init(navigationController: UINavigationController, - source: SignInSource?, - loginFields: LoginFields, - tracker: AuthenticatorAnalyticsTracker, - configuration: WordPressAuthenticatorConfiguration) { - self.navigationController = navigationController - self.source = source - self.loginFields = loginFields - self.tracker = tracker - self.configuration = configuration - } - - func start() async { - if configuration.isWPComMagicLinkPreferredToPassword { - let result = await requestMagicLink() - switch result { - case .success: - loginFields.restrictToWPCom = true - showMagicLinkRequested() - case .failure(let error): - // When magic link request fails, falls back to the password flow. - showPassword() - tracker.track(failure: error.localizedDescription) - } - } else { - showPassword() - } - } -} - -private extension PasswordCoordinator { - /// Makes the call to request a magic authentication link be emailed to the user. - func requestMagicLink() async -> Result { - loginFields.meta.emailMagicLinkSource = .login - return await MagicLinkRequester().requestMagicLink(email: loginFields.username, jetpackLogin: loginFields.meta.jetpackLogin) - } - - /// After a magic link is successfully sent, navigates the user to the requested screen. - func showMagicLinkRequested() { - let vc = MagicLinkRequestedViewController(email: loginFields.username) { [weak self] in - self?.showPassword() - } - navigationController?.pushViewController(vc, animated: true) - } - - /// Navigates the user to enter WP.com password. - func showPassword() { - guard let vc = PasswordViewController.instantiate(from: .password) else { - return WPLogError("Failed to navigate to PasswordViewController from GetStartedViewController") - } - - vc.source = source - vc.loginFields = loginFields - vc.trackAsPasswordChallenge = false - - navigationController?.pushViewController(vc, animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/PasswordViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/PasswordViewController.swift deleted file mode 100644 index e35e87318c58..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/PasswordViewController.swift +++ /dev/null @@ -1,614 +0,0 @@ -import UIKit -import WordPressKit -import WordPressShared - -/// PasswordViewController: view to enter WP account password. -/// -class PasswordViewController: LoginViewController { - - // MARK: - Properties - - @IBOutlet private weak var tableView: UITableView! - @IBOutlet var bottomContentConstraint: NSLayoutConstraint? - @IBOutlet private weak var secondaryButton: NUXButton! - - private weak var passwordField: UITextField? - private var rows = [Row]() - private var errorMessage: String? - private var shouldChangeVoiceOverFocus: Bool = false - private var loginLinkCell: TextLinkButtonTableViewCell? - - private let isMagicLinkShownAsSecondaryAction: Bool = WordPressAuthenticator.shared.configuration.isWPComMagicLinkShownAsSecondaryActionOnPasswordScreen - - private let configuration = WordPressAuthenticator.shared.configuration - - /// Depending on where we're coming from, this screen needs to track a password challenge - /// (if logging on with a Social account) or not (if logging in through WP.com). - /// - var trackAsPasswordChallenge = true - - var source: SignInSource? - - override var loginFields: LoginFields { - didSet { - loginFields.password = "" - } - } - - override var sourceTag: WordPressSupportSourceTag { - get { - return .loginWPComPassword - } - } - - // Required for `NUXKeyboardResponder` but unused here. - var verticalCenterConstraint: NSLayoutConstraint? - - // MARK: - View - - override func viewDidLoad() { - super.viewDidLoad() - - removeGoogleWaitingView() - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.logInTitle - - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - - configureLoginWithMagicLinkButton() - localizePrimaryButton() - registerTableViewCells() - loadRows() - configureForAccessibility() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - loginFields.meta.userIsDotCom = true - configureSubmitButton(animating: false) - loginLinkCell?.enableButton(true) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if trackAsPasswordChallenge { - if isMovingToParent { - tracker.track(step: .passwordChallenge) - } else { - tracker.set(step: .passwordChallenge) - } - } else { - tracker.set(flow: isMagicLinkShownAsSecondaryAction ? .loginWithPasswordWithMagicLinkEmphasis : .loginWithPassword) - - if isMovingToParent { - tracker.track(step: .start) - } else { - tracker.set(step: .start) - } - } - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - - configureViewForEditingIfNeeded() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - } - - // MARK: - Overrides - - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? - WordPressAuthenticator.shared.style.statusBarStyle - } - - override func configureViewLoading(_ loading: Bool) { - super.configureViewLoading(loading) - passwordField?.isEnabled = !loading - } - - override func displayRemoteError(_ error: Error) { - configureViewLoading(false) - - if let source, loginFields.meta.userIsDotCom { - let passwordError = SignInError.invalidWPComPassword(source: source) - if authenticationDelegate.shouldHandleError(passwordError) { - authenticationDelegate.handleError(passwordError) { _ in - // No custom navigation is expected in this case. - } - } - } - - if let oauthError = error as? WordPressComOAuthError, case let .endpointError(failure) = oauthError, failure.kind == .invalidRequest { - // The only difference between an incorrect password error and exceeded login limit error - // is the actual error string. So check for "password" in the error string, and show the custom - // error message. Otherwise, show the actual response error. - var displayMessage: String { - // swiftlint:disable localization_comment - if let msg = failure.localizedErrorMessage, msg.contains(NSLocalizedString("password", comment: "")) { - // swiftlint:enable localization_comment - return NSLocalizedString("It seems like you've entered an incorrect password. Want to give it another try?", comment: "An error message shown when a wpcom user provides the wrong password.") - } - if let msg = failure.localizedErrorMessage { - return msg - } - return oauthError.localizedDescription - } - displayError(message: displayMessage, moveVoiceOverFocus: true) - } else { - displayError(error, sourceTag: sourceTag) - } - } - - override func displayError(message: String, moveVoiceOverFocus: Bool = false) { - // The reason why this check is necessary is that we're calling this method - // with an empty error message when setting up the VC. We don't want to track - // an empty error when that happens. - if !message.isEmpty { - tracker.track(failure: message) - } - - configureViewLoading(false) - - if errorMessage != message { - errorMessage = message - shouldChangeVoiceOverFocus = moveVoiceOverFocus - loadRows() - tableView.reloadData() - } - } - - override func validateFormAndLogin() { - view.endEditing(true) - displayError(message: "", moveVoiceOverFocus: true) - - // Is everything filled out? - if !loginFields.validateFieldsPopulatedForSignin() { - let errorMsg = Localization.missingInfoError - displayError(message: errorMsg, moveVoiceOverFocus: true) - - return - } - - configureViewLoading(true) - - loginFacade.signIn(with: loginFields) - } -} - -// MARK: - LoginFacadeDelegate - -extension PasswordViewController { - // Used when the account has support for security keys. - // - func needsMultifactorCode(forUserID userID: Int, andNonceInfo nonceInfo: SocialLogin2FANonceInfo) { - configureViewLoading(false) - socialNeedsMultifactorCode(forUserID: userID, andNonceInfo: nonceInfo) - } -} - -// MARK: - Validation and Continue - -private extension PasswordViewController { - - // MARK: - Button Actions - - @IBAction func handleContinueButtonTapped(_ sender: NUXButton) { - tracker.track(click: .submit) - - configureViewLoading(true) - validateForm() - } - - func validateForm() { - validateFormAndLogin() - } -} - -// MARK: - UITextFieldDelegate - -extension PasswordViewController: UITextFieldDelegate { - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if enableSubmit(animating: false) { - validateForm() - } - return true - } -} - -// MARK: - UITableViewDataSource - -extension PasswordViewController: UITableViewDataSource { - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return rows.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - return cell - } -} - -// MARK: - Keyboard Notifications - -extension PasswordViewController: NUXKeyboardResponder { - - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } -} - -// MARK: - Magic Link - -private extension PasswordViewController { - func configureLoginWithMagicLinkButton() { - if isMagicLinkShownAsSecondaryAction { - secondaryButton.setTitle(Localization.loginWithMagicLink, for: .normal) - secondaryButton.accessibilityIdentifier = AccessibilityIdentifier.loginWithMagicLink - secondaryButton.on(.touchUpInside) { [weak self] _ in - Task { @MainActor [weak self] in - guard let self else { return } - self.secondaryButton.isEnabled = false - await self.loginWithMagicLink() - self.secondaryButton.isEnabled = true - } - } - } else { - secondaryButton.isHidden = true - } - } - - func loginWithMagicLink() async { - tracker.track(click: .requestMagicLink) - loginFields.meta.emailMagicLinkSource = .login - - updateLoadingUI(isRequestingMagicLink: true) - let result = await MagicLinkRequester().requestMagicLink(email: loginFields.username, jetpackLogin: loginFields.meta.jetpackLogin) - switch result { - case .success: - didRequestAuthenticationLink() - case .failure(let error): - switch error { - case MagicLinkRequester.MagicLinkRequestError.invalidEmail: - WPLogError("Attempted to request authentication link, but the email address did not appear valid.") - let alert = buildInvalidEmailAlert() - present(alert, animated: true, completion: nil) - default: - tracker.track(failure: error.localizedDescription) - displayError(error, sourceTag: sourceTag) - } - } - updateLoadingUI(isRequestingMagicLink: false) - } - - func updateLoadingUI(isRequestingMagicLink: Bool) { - if isRequestingMagicLink { - if isMagicLinkShownAsSecondaryAction { - submitButton?.isEnabled = false - secondaryButton.showActivityIndicator(true) - } else { - configureViewLoading(true) - } - } else { - if isMagicLinkShownAsSecondaryAction { - submitButton?.isEnabled = true - secondaryButton.showActivityIndicator(false) - } else { - configureViewLoading(false) - } - } - } -} - -// MARK: - Table Management - -private extension PasswordViewController { - - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - GravatarEmailTableViewCell.reuseIdentifier: GravatarEmailTableViewCell.loadNib(), - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib(), - TextFieldTableViewCell.reuseIdentifier: TextFieldTableViewCell.loadNib(), - TextLinkButtonTableViewCell.reuseIdentifier: TextLinkButtonTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - } - - /// Describes how the tableView rows should be rendered. - /// - func loadRows() { - rows = [.gravatarEmail] - - // Instructions only for social accounts and simplified WPCom login flow - if loginFields.meta.socialService != nil || - configuration.wpcomPasswordInstructions != nil { - rows.append(.instructions) - } - - rows.append(.password) - - if let errorText = errorMessage, !errorText.isEmpty { - rows.append(.errorMessage) - } - - rows.append(.forgotPassword) - - if !isMagicLinkShownAsSecondaryAction { - rows.append(.sendMagicLink) - } - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as GravatarEmailTableViewCell: - configureGravatarEmail(cell) - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextFieldTableViewCell where row == .password: - configurePasswordTextField(cell) - case let cell as TextLinkButtonTableViewCell where row == .forgotPassword: - configureForgotPasswordButton(cell) - case let cell as TextLinkButtonTableViewCell where row == .sendMagicLink: - configureSendMagicLinkButton(cell) - case let cell as TextLabelTableViewCell where row == .errorMessage: - configureErrorLabel(cell) - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the gravatar + email cell. - /// - func configureGravatarEmail(_ cell: GravatarEmailTableViewCell) { - cell.configure(withEmail: loginFields.username, hasBorders: configuration.emphasizeEmailForWPComPassword) - - cell.onChangeSelectionHandler = { [weak self] textfield in - // The email can only be changed via a password manager. - // In this case, don't update username for social accounts. - // This prevents inadvertent account linking. - if self?.loginFields.meta.socialService != nil { - cell.updateEmailAddress(self?.loginFields.username) - } else { - self?.loginFields.username = textfield.nonNilTrimmedText() - self?.loginFields.emailAddress = textfield.nonNilTrimmedText() - } - - self?.configureSubmitButton(animating: false) - } - } - - /// Configure the instruction cell for social accounts or simplified login. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - let displayStrings = WordPressAuthenticator.shared.displayStrings - let instructions: String? = { - if let service = loginFields.meta.socialService { - return (service == .google) ? displayStrings.googlePasswordInstructions : - displayStrings.applePasswordInstructions - } - return configuration.wpcomPasswordInstructions - }() - - guard let instructions else { - return - } - - cell.configureLabel(text: instructions) - } - - /// Configure the password textfield cell. - /// - func configurePasswordTextField(_ cell: TextFieldTableViewCell) { - cell.configure(withStyle: .password, - placeholder: WordPressAuthenticator.shared.displayStrings.passwordPlaceholder) - - // Save a reference to the first textField so it can becomeFirstResponder. - passwordField = cell.textField - cell.textField.delegate = self - - cell.onChangeSelectionHandler = { [weak self] textfield in - self?.loginFields.password = textfield.nonNilTrimmedText() - self?.configureSubmitButton(animating: false) - } - - SigninEditingState.signinEditingStateActive = true - - if UIAccessibility.isVoiceOverRunning { - // Quiet repetitive VoiceOver elements. - passwordField?.placeholder = nil - } - } - - /// Configure the forgot password link cell. - /// - func configureForgotPasswordButton(_ cell: TextLinkButtonTableViewCell) { - cell.configureButton(text: WordPressAuthenticator.shared.displayStrings.resetPasswordButtonTitle, - accessibilityTrait: .link, - showBorder: true) - cell.actionHandler = { [weak self] in - guard let self else { - return - } - - self.tracker.track(click: .forgottenPassword) - - // If information is currently processing, ignore button tap. - guard self.enableSubmit(animating: false) else { - return - } - - WordPressAuthenticator.openForgotPasswordURL(self.loginFields) - } - } - - /// Configure the "send magic link" cell. - /// - func configureSendMagicLinkButton(_ cell: TextLinkButtonTableViewCell) { - cell.configureButton(text: WordPressAuthenticator.shared.displayStrings.getLoginLinkButtonTitle, - accessibilityTrait: .link, - showBorder: true) - cell.accessibilityIdentifier = AccessibilityIdentifier.loginWithMagicLink - - // Save reference to the login link cell so it can be enabled/disabled. - loginLinkCell = cell - - cell.actionHandler = { [weak self] in - guard let self else { - return - } - - cell.enableButton(false) - - Task { @MainActor [weak self] in - await self?.loginWithMagicLink() - } - } - } - - /// Configure the error message cell. - /// - func configureErrorLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: errorMessage, style: .error) - cell.accessibilityIdentifier = "Password Error" - if shouldChangeVoiceOverFocus { - UIAccessibility.post(notification: .layoutChanged, argument: cell) - } - } - - /// Configure the view for an editing state. - /// - func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - passwordField?.becomeFirstResponder() - } - } - - /// Sets up accessibility elements in the order which they should be read aloud - /// and chooses which element to focus on at the beginning. - /// - func configureForAccessibility() { - view.accessibilityElements = [ - passwordField as Any, - tableView as Any, - submitButton as Any - ] - - if isMagicLinkShownAsSecondaryAction { - view.accessibilityElements?.append(secondaryButton as Any) - } - - UIAccessibility.post(notification: .screenChanged, argument: passwordField) - } - - /// When a magic link successfully sends, navigate the user to the next step. - /// - func didRequestAuthenticationLink() { - guard let vc = LoginMagicLinkViewController.instantiate(from: .unifiedLoginMagicLink) else { - WPLogError("Failed to navigate to LoginMagicLinkViewController") - return - } - - vc.loginFields = self.loginFields - vc.loginFields.restrictToWPCom = true - navigationController?.pushViewController(vc, animated: true) - } - - /// Build the alert message when the email address is invalid. - /// - func buildInvalidEmailAlert() -> UIAlertController { - let title = NSLocalizedString("Can Not Request Link", - comment: "Title of an alert letting the user know") - let message = NSLocalizedString("A valid email address is needed to mail an authentication link. Please return to the previous screen and provide a valid email address.", - comment: "An error message.") - let helpActionTitle = NSLocalizedString("Need help?", - comment: "Takes the user to get help") - let okActionTitle = NSLocalizedString("OK", - comment: "Dismisses the alert") - let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - - alert.addActionWithTitle(helpActionTitle, - style: .cancel, - handler: { _ in - WordPressAuthenticator.shared.delegate?.presentSupportRequest(from: self, sourceTag: .loginEmail) - }) - - alert.addActionWithTitle(okActionTitle, style: .default, handler: nil) - - return alert - } - - /// Rows listed in the order they were created. - /// - enum Row { - case gravatarEmail - case instructions - case password - case forgotPassword - case sendMagicLink - case errorMessage - - var reuseIdentifier: String { - switch self { - case .gravatarEmail: - return GravatarEmailTableViewCell.reuseIdentifier - case .instructions: - return TextLabelTableViewCell.reuseIdentifier - case .password: - return TextFieldTableViewCell.reuseIdentifier - case .sendMagicLink: - return TextLinkButtonTableViewCell.reuseIdentifier - case .forgotPassword: - return TextLinkButtonTableViewCell.reuseIdentifier - case .errorMessage: - return TextLabelTableViewCell.reuseIdentifier - } - } - } -} - -private extension PasswordViewController { - /// Localization constants - /// - enum Localization { - static let missingInfoError = NSLocalizedString("Please fill out all the fields", - comment: "A short prompt asking the user to properly fill out all login fields.") - static let loginWithMagicLink = NSLocalizedString("Or log in with magic link", - comment: "The button title for a secondary call-to-action button on the password screen. When the user wants to try sending a magic link instead of entering a password.") - } - - enum AccessibilityIdentifier { - static let loginWithMagicLink = "Get Login Link Button" - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/GravatarEmailTableViewCell.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/GravatarEmailTableViewCell.swift deleted file mode 100644 index ff323fecaf0a..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/GravatarEmailTableViewCell.swift +++ /dev/null @@ -1,106 +0,0 @@ -import UIKit - -/// GravatarEmailTableViewCell: Gravatar image + Email address in a UITableViewCell. -/// -class GravatarEmailTableViewCell: UITableViewCell { - - /// Private properties - /// - @IBOutlet private weak var gravatarImageView: UIImageView? - @IBOutlet private weak var emailLabel: UITextField? - @IBOutlet private var containerView: UIView! - - @IBOutlet private var containerViewMargins: [NSLayoutConstraint]! - @IBOutlet private var gravatarImageViewSizeConstraints: [NSLayoutConstraint]! - - private let gridiconSize = CGSize(width: 48, height: 48) - private let girdiconSmallSize = CGSize(width: 32, height: 32) - - /// Public properties - /// - public static let reuseIdentifier = "GravatarEmailTableViewCell" - public var onChangeSelectionHandler: ((_ sender: UITextField) -> Void)? - private var gravatarPlaceholderImage: UIImage? = nil - private var gravatarPreferredSize: CGSize = .zero - private var email: String? - - required init?(coder: NSCoder) { - super.init(coder: coder) - NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarQEAvatarUpdateNotification, object: nil) - } - - /// Public Methods - /// - public func configure(withEmail email: String?, andPlaceholder placeholderImage: UIImage? = nil, hasBorders: Bool = false) { - self.email = email - gravatarImageView?.tintColor = WordPressAuthenticator.shared.unifiedStyle?.borderColor ?? WordPressAuthenticator.shared.style.primaryNormalBorderColor - emailLabel?.textColor = WordPressAuthenticator.shared.unifiedStyle?.gravatarEmailTextColor ?? WordPressAuthenticator.shared.unifiedStyle?.textSubtleColor ?? WordPressAuthenticator.shared.style.subheadlineColor - emailLabel?.font = UIFont.preferredFont(forTextStyle: .body) - emailLabel?.text = email - - let gridicon: UIImage = .gridicon(.userCircle, size: hasBorders ? girdiconSmallSize : gridiconSize) - - guard let email, - email.isValidEmail() else { - gravatarImageView?.image = gridicon - return - } - self.gravatarPlaceholderImage = placeholderImage ?? gridicon - self.gravatarPreferredSize = gridicon.size - Task { - try await downloadAvatar() - } - - gravatarImageViewSizeConstraints.forEach { constraint in - constraint.constant = gridicon.size.width - } - - let margin: CGFloat = hasBorders ? 16 : 0 - containerViewMargins.forEach { constraint in - constraint.constant = margin - } - - containerView.layer.borderWidth = hasBorders ? 1 : 0 - containerView.layer.cornerRadius = hasBorders ? 8 : 0 - containerView.layer.borderColor = hasBorders ? UIColor.systemGray3.cgColor : UIColor.clear.cgColor - } - - @objc private func refreshAvatar(_ notification: Foundation.Notification) { - guard let email, notification.userInfoHasEmail(email) else { return } - Task { - try await downloadAvatar(forceRefresh: true) - } - } - - private func downloadAvatar(forceRefresh: Bool = false) async throws { - guard let email, let gravatarPlaceholderImage else { return } - try await gravatarImageView?.setGravatarImage(with: email, placeholder: gravatarPlaceholderImage, preferredSize: gravatarPreferredSize, forceRefresh: forceRefresh) - } - - func updateEmailAddress(_ email: String?) { - emailLabel?.text = email - } -} - -// MARK: - Password Manager Handling - -private extension GravatarEmailTableViewCell { - - // MARK: - All Password Managers - - /// Call the handler when the text field changes. - /// - /// - Note: we have to manually add an action to the textfield - /// because the delegate method `textFieldDidChangeSelection(_ textField: UITextField)` - /// is only available to iOS 13+. When we no longer support iOS 12, - /// `textFieldDidChangeSelection`, and `onChangeSelectionHandler` can - /// be deleted in favor of adding the delegate method to view controllers. - /// - @IBAction func textFieldDidChangeSelection() { - guard let emailTextField = emailLabel else { - return - } - - onChangeSelectionHandler?(emailTextField) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/GravatarEmailTableViewCell.xib b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/GravatarEmailTableViewCell.xib deleted file mode 100644 index 49db946227c5..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/GravatarEmailTableViewCell.xib +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextFieldTableViewCell.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextFieldTableViewCell.swift deleted file mode 100644 index 509631504805..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextFieldTableViewCell.swift +++ /dev/null @@ -1,237 +0,0 @@ -import UIKit -import WordPressShared - -/// TextFieldTableViewCell: a textfield with a custom border line in a UITableViewCell. -/// -final class TextFieldTableViewCell: UITableViewCell { - - /// Private properties. - /// - @IBOutlet private weak var borderView: UIView! - @IBOutlet private weak var borderWidth: NSLayoutConstraint! - private var secureTextEntryToggle: UIButton? - private var secureTextEntryImageVisible: UIImage? - private var secureTextEntryImageHidden: UIImage? - private var textfieldStyle: TextFieldStyle = .url - - /// Register an action for the SiteAddress URL textfield. - /// - Note: we have to manually add an action to the textfield - /// because the delegate method `textFieldDidChangeSelection(_ textField: UITextField)` - /// is only available to iOS 13+. When we no longer support iOS 12, - /// `registerTextFieldAction`, `textFieldDidChangeSelection`, and `onChangeSelectionHandler` can - /// be deleted in favor of adding the delegate method to SiteAddressViewController. - @IBAction func registerTextFieldAction() { - onChangeSelectionHandler?(textField) - } - - /// Public properties. - /// - @IBOutlet public weak var textField: UITextField! // public so it can be the first responder - @IBInspectable public var showSecureTextEntryToggle: Bool = false { - didSet { - configureSecureTextEntryToggle() - } - } - - public var onChangeSelectionHandler: ((_ sender: UITextField) -> Void)? - public static let reuseIdentifier = "TextFieldTableViewCell" - - override func awakeFromNib() { - super.awakeFromNib() - styleBorder() - setCommonTextFieldStyles() - } - - /// Configures the textfield for URL, username, or entering a password. - /// - Parameter style: changes the textfield behavior and appearance. - /// - Parameter placeholder: the placeholder text, if any. - /// - Parameter text: the field text, if any. - /// - public func configure(withStyle style: TextFieldStyle = .url, placeholder: String? = nil, text: String? = nil) { - textfieldStyle = style - applyTextFieldStyle(style) - textField.placeholder = placeholder - textField.text = text - } - - override func prepareForReuse() { - super.prepareForReuse() - - textField.keyboardType = .default - textField.returnKeyType = .default - setSecureTextEntry(false) - showSecureTextEntryToggle = false - textField.rightView = nil - textField.accessibilityLabel = nil - textField.accessibilityIdentifier = nil - } -} - -// MARK: - Private methods -private extension TextFieldTableViewCell { - - /// Style the bottom cell border, called borderView. - /// - func styleBorder() { - let borderColor = WordPressAuthenticator.shared.unifiedStyle?.borderColor ?? WordPressAuthenticator.shared.style.primaryNormalBorderColor - borderView.backgroundColor = borderColor - borderWidth.constant = WPStyleGuide.hairlineBorderWidth - } - - /// Apply common keyboard traits and font styles. - /// - func setCommonTextFieldStyles() { - textField.font = UIFont.preferredFont(forTextStyle: .body) - textField.autocorrectionType = .no - } - - /// Sets the textfield keyboard type and applies common traits. - /// - note: Don't assign first responder here. It's too early in the view lifecycle. - /// - func applyTextFieldStyle(_ style: TextFieldStyle) { - switch style { - case .url: - textField.keyboardType = .URL - textField.returnKeyType = .continue - registerTextFieldAction() - textField.accessibilityLabel = Constants.siteAddress - textField.accessibilityIdentifier = Constants.siteAddressID - case .username: - textField.keyboardType = .default - textField.returnKeyType = .next - textField.accessibilityLabel = Constants.username - textField.accessibilityIdentifier = Constants.usernameID - case .password: - textField.keyboardType = .default - textField.returnKeyType = .continue - setSecureTextEntry(true) - showSecureTextEntryToggle = true - configureSecureTextEntryToggle() - textField.accessibilityLabel = Constants.password - textField.accessibilityIdentifier = Constants.passwordID - case .numericCode: - textField.keyboardType = .numberPad - textField.returnKeyType = .continue - textField.accessibilityLabel = Constants.otp - textField.accessibilityIdentifier = Constants.otpID - case .email: - textField.keyboardType = .emailAddress - textField.returnKeyType = .continue - textField.textContentType = .username // So the password autofill appears on the keyboard - textField.accessibilityLabel = Constants.email - textField.accessibilityIdentifier = Constants.emailID - } - if WordPressAuthenticator.shared.configuration.disableAutofill { - textField.textContentType = nil - } - } - - /// Call the handler when the textfield changes. - /// - @objc func textFieldDidChangeSelection() { - onChangeSelectionHandler?(textField) - } -} - -// MARK: - Secure Text Entry -/// Methods ported from WPWalkthroughTextField.h/.m -/// -private extension TextFieldTableViewCell { - - /// Build the show / hide icon in the textfield. - /// - func configureSecureTextEntryToggle() { - guard showSecureTextEntryToggle else { - return - } - - secureTextEntryImageVisible = UIImage.gridicon(.visible) - secureTextEntryImageHidden = UIImage.gridicon(.notVisible) - - secureTextEntryToggle = UIButton(type: .custom) - secureTextEntryToggle?.clipsToBounds = true - // The icon should match the border color. - let tintColor = WordPressAuthenticator.shared.unifiedStyle?.borderColor ?? WordPressAuthenticator.shared.style.primaryNormalBorderColor - secureTextEntryToggle?.tintColor = tintColor - - secureTextEntryToggle?.addTarget(self, - action: #selector(secureTextEntryToggleAction), - for: .touchUpInside) - - updateSecureTextEntryToggleImage() - updateSecureTextEntryForAccessibility() - textField.rightView = secureTextEntryToggle - textField.rightViewMode = .always - } - - func setSecureTextEntry(_ secureTextEntry: Bool) { - textField.font = UIFont.preferredFont(forTextStyle: .body) - - textField.isSecureTextEntry = secureTextEntry - updateSecureTextEntryToggleImage() - updateSecureTextEntryForAccessibility() - } - - @objc func secureTextEntryToggleAction(_ sender: Any) { - textField.isSecureTextEntry.toggle() - - // Save and re-apply the current selection range to save the cursor position - let currentTextRange = textField.selectedTextRange - textField.becomeFirstResponder() - textField.selectedTextRange = currentTextRange - updateSecureTextEntryToggleImage() - updateSecureTextEntryForAccessibility() - } - - func updateSecureTextEntryToggleImage() { - let image = textField.isSecureTextEntry ? secureTextEntryImageHidden : secureTextEntryImageVisible - secureTextEntryToggle?.setImage(image, for: .normal) - secureTextEntryToggle?.sizeToFit() - } - - func updateSecureTextEntryForAccessibility() { - secureTextEntryToggle?.accessibilityLabel = Constants.showPassword - secureTextEntryToggle?.accessibilityIdentifier = Constants.showPassword - secureTextEntryToggle?.accessibilityValue = textField.isSecureTextEntry ? Constants.passwordHidden : Constants.passwordShown - } -} - -// MARK: - Constants -extension TextFieldTableViewCell { - - /// TextField configuration options. - /// - enum TextFieldStyle { - case url - case username - case password - case numericCode - case email - } - - struct Constants { - /// Accessibility Hints - /// - static let passwordHidden = NSLocalizedString("Hidden", - comment: "Accessibility value if login page's password field is hiding the password (i.e. with asterisks).") - static let passwordShown = NSLocalizedString("Shown", - comment: "Accessibility value if login page's password field is displaying the password.") - static let showPassword = NSLocalizedString("Show password", - comment: "Accessibility label for the 'Show password' button in the login page's password field.") - static let siteAddress = NSLocalizedString("Site address", - comment: "Accessibility label of the site address field shown when adding a self-hosted site.") - static let username = NSLocalizedString("Username", - comment: "Accessibility label for the username text field in the self-hosted login page.") - static let password = NSLocalizedString("Password", - comment: "Accessibility label for the password text field in the self-hosted login page.") - static let otp = NSLocalizedString("Authentication code", - comment: "Accessibility label for the 2FA text field.") - static let email = NSLocalizedString("Email address", - comment: "Accessibility label for the email address text field.") - static let siteAddressID = "Site address" - static let usernameID = "Username" - static let passwordID = "Password" - static let otpID = "Authentication code" - static let emailID = "Email address" - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextFieldTableViewCell.xib b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextFieldTableViewCell.xib deleted file mode 100644 index c0ada44fb922..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextFieldTableViewCell.xib +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLabelTableViewCell.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLabelTableViewCell.swift deleted file mode 100644 index 59ec4ba96bd7..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLabelTableViewCell.swift +++ /dev/null @@ -1,43 +0,0 @@ -import UIKit - -/// TextLabelTableViewCell: a text label in a UITableViewCell. -/// -public final class TextLabelTableViewCell: UITableViewCell { - - /// Private properties - /// - @IBOutlet private weak var label: UILabel! - - /// Public properties - /// - public static let reuseIdentifier = "TextLabelTableViewCell" - - public func configureLabel(text: String?, style: TextLabelStyle = .body) { - label.text = text - - switch style { - case .body: - label.textColor = WordPressAuthenticator.shared.unifiedStyle?.textColor ?? WordPressAuthenticator.shared.style.instructionColor - label.font = UIFont.preferredFont(forTextStyle: .body) - case .error: - label.textColor = WordPressAuthenticator.shared.unifiedStyle?.errorColor ?? UIColor.red - label.font = UIFont.preferredFont(forTextStyle: .body) - } - } - - /// Override methods - /// - public override func prepareForReuse() { - super.prepareForReuse() - label.text = nil - } -} - -public extension TextLabelTableViewCell { - /// The label style to display - /// - enum TextLabelStyle { - case body - case error - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLabelTableViewCell.xib b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLabelTableViewCell.xib deleted file mode 100644 index 734e76ccc2ff..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLabelTableViewCell.xib +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLinkButtonTableViewCell.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLinkButtonTableViewCell.swift deleted file mode 100644 index 96bdf661e504..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLinkButtonTableViewCell.swift +++ /dev/null @@ -1,76 +0,0 @@ -import UIKit -import WordPressShared - -/// TextLinkButtonTableViewCell: a plain button made to look like a text link. -/// -class TextLinkButtonTableViewCell: UITableViewCell { - - /// Private properties - /// - @IBOutlet private weak var iconView: UIImageView! - @IBOutlet private weak var button: UIButton! - @IBOutlet private weak var borderView: UIView! - @IBOutlet private weak var borderWidth: NSLayoutConstraint! - @IBAction private func textLinkButtonTapped(_ sender: UIButton) { - actionHandler?() - } - - /// Public properties - /// - public static let reuseIdentifier = "TextLinkButtonTableViewCell" - - public var actionHandler: (() -> Void)? - - override func awakeFromNib() { - super.awakeFromNib() - - button.titleLabel?.adjustsFontForContentSizeCategory = true - styleBorder() - } - - public func configureButton(text: String?, - icon: UIImage? = nil, - accessibilityTrait: UIAccessibilityTraits = .button, - showBorder: Bool = false, - accessibilityIdentifier: String? = nil) { - button.setTitle(text, for: .normal) - - let buttonTitleColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonColor ?? WordPressAuthenticator.shared.style.textButtonColor - let buttonHighlightColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonHighlightColor ?? WordPressAuthenticator.shared.style.textButtonHighlightColor - button.setTitleColor(buttonTitleColor, for: .normal) - button.setTitleColor(buttonHighlightColor, for: .highlighted) - button.accessibilityTraits = accessibilityTrait - button.accessibilityIdentifier = accessibilityIdentifier - - borderView.isHidden = !showBorder - - iconView.image = icon - iconView.isHidden = icon == nil - iconView.tintColor = buttonTitleColor - } - - /// Toggle button enabled / disabled - /// - public func enableButton(_ isEnabled: Bool) { - button.isEnabled = isEnabled - } -} - -// MARK: - Private methods -private extension TextLinkButtonTableViewCell { - - /// Style the bottom cell border, called borderView. - /// - func styleBorder() { - let borderColor = WordPressAuthenticator.shared.unifiedStyle?.borderColor ?? WordPressAuthenticator.shared.style.primaryNormalBorderColor - borderView.backgroundColor = borderColor - borderWidth.constant = WPStyleGuide.hairlineBorderWidth - } -} - -// MARK: - Constants -extension TextLinkButtonTableViewCell { - struct Constants { - static let passkeysID = "Passkeys" - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLinkButtonTableViewCell.xib b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLinkButtonTableViewCell.xib deleted file mode 100644 index 37d749d71ce0..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLinkButtonTableViewCell.xib +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextWithLinkTableViewCell.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextWithLinkTableViewCell.swift deleted file mode 100644 index 9028f54fa91d..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextWithLinkTableViewCell.swift +++ /dev/null @@ -1,43 +0,0 @@ -import UIKit - -/// TextWithLinkTableViewCell: a button with the title regular text and an underlined link. -/// -class TextWithLinkTableViewCell: UITableViewCell { - - /// Public properties - /// - static let reuseIdentifier = "TextWithLinkTableViewCell" - var actionHandler: (() -> Void)? - - /// Private properties - /// - @IBOutlet private weak var button: UIButton! - @IBAction private func buttonTapped(_ sender: UIButton) { - actionHandler?() - } - - override func awakeFromNib() { - super.awakeFromNib() - button.titleLabel?.adjustsFontForContentSizeCategory = true - } - - /// Creates an attributed string from the provided marked text and assigns it to the button title. - /// - /// - Parameters: - /// - markedText: string with the text to be formatted as a link marked with "_". - /// Example: "this _is_ a link" will format "is" as an underlined link. - /// - accessibilityTrait: accessibilityTrait of button (optional) - /// - func configureButton(markedText text: String, accessibilityTrait: UIAccessibilityTraits = .link) { - let textColor = WordPressAuthenticator.shared.unifiedStyle?.textSubtleColor ?? WordPressAuthenticator.shared.style.subheadlineColor - let linkColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonColor ?? WordPressAuthenticator.shared.style.textButtonColor - let linkHighlightColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonHighlightColor ?? WordPressAuthenticator.shared.style.textButtonHighlightColor - - let attributedString = text.underlined(color: textColor, underlineColor: linkColor) - let highlightAttributedString = text.underlined(color: textColor, underlineColor: linkHighlightColor) - - button.setAttributedTitle(attributedString, for: .normal) - button.setAttributedTitle(highlightAttributedString, for: .highlighted) - button.accessibilityTraits = accessibilityTrait - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextWithLinkTableViewCell.xib b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextWithLinkTableViewCell.xib deleted file mode 100644 index db080cb388b9..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextWithLinkTableViewCell.xib +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/SignupMagicLinkViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/SignupMagicLinkViewController.swift deleted file mode 100644 index a571b8c300ac..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/SignupMagicLinkViewController.swift +++ /dev/null @@ -1,195 +0,0 @@ -import UIKit -import WordPressShared - -/// SignupMagicLinkViewController: step two in the signup flow. -/// This VC prompts the user to open their email app to look for the magic link we sent. -/// -final class SignupMagicLinkViewController: LoginViewController { - - // MARK: Properties - - @IBOutlet private weak var tableView: UITableView! - private var rows = [Row]() - private var errorMessage: String? - private var shouldChangeVoiceOverFocus: Bool = false - - override var sourceTag: WordPressSupportSourceTag { - get { - return .wpComSignupMagicLink - } - } - - // MARK: - Actions - @IBAction func handleContinueButtonTapped(_ sender: NUXButton) { - tracker.track(click: .openEmailClient) - tracker.track(step: .emailOpened) - - let linkMailPresenter = LinkMailPresenter(emailAddress: loginFields.username) - let appSelector = AppSelector(sourceView: sender) - linkMailPresenter.presentEmailClients(on: self, appSelector: appSelector) - } - - // MARK: - View lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - - validationCheck() - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.signUpTitle - - // Store default margin, and size table for the view. - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - - localizePrimaryButton() - registerTableViewCells() - loadRows() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if isMovingToParent { - tracker.track(step: .magicLinkRequested) - } else { - tracker.set(step: .magicLinkRequested) - } - } - - /// Validation check while we are bypassing screens. - /// - func validationCheck() { - let email = loginFields.username - if !email.isValidEmail() { - WPLogError("The value of loginFields.username was not a valid email address.") - } - } - - // MARK: - Overrides - - /// Style individual ViewController backgrounds, for now. - /// - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - /// Style individual ViewController status bars. - /// - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } - - /// Override the title on 'submit' button - /// - override func localizePrimaryButton() { - submitButton?.setTitle(WordPressAuthenticator.shared.displayStrings.openMailButtonTitle, for: .normal) - } -} - -// MARK: - UITableViewDataSource -extension SignupMagicLinkViewController: UITableViewDataSource { - /// Returns the number of rows in a section. - /// - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return rows.count - } - - /// Configure cells delegate method. - /// - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - - return cell - } -} - -// MARK: - Private Methods -private extension SignupMagicLinkViewController { - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - GravatarEmailTableViewCell.reuseIdentifier: GravatarEmailTableViewCell.loadNib(), - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - } - - /// Describes how the tableView rows should be rendered. - /// - func loadRows() { - rows = [.persona, .instructions, .checkSpam, .oops] - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as GravatarEmailTableViewCell where row == .persona: - configureGravatarEmail(cell) - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextLabelTableViewCell where row == .checkSpam: - configureCheckSpamLabel(cell) - case let cell as TextLabelTableViewCell where row == .oops: - configureoopsLabel(cell) - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the gravatar + email cell. - /// - func configureGravatarEmail(_ cell: GravatarEmailTableViewCell) { - cell.configure(withEmail: loginFields.username) - } - - /// Configure the instruction cell. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.openMailSignupInstructions, style: .body) - } - - /// Configure the "Check spam" cell. - /// - func configureCheckSpamLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.checkSpamInstructions, style: .body) - } - - /// Configure the "Check spam" cell. - /// - func configureoopsLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.oopsInstructions, style: .body) - } - - // MARK: - Private Constants - - /// Rows listed in the order they were created. - /// - enum Row { - case persona - case instructions - case checkSpam - case oops - - var reuseIdentifier: String { - switch self { - case .persona: - return GravatarEmailTableViewCell.reuseIdentifier - case .instructions, .checkSpam, .oops: - return TextLabelTableViewCell.reuseIdentifier - } - } - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/UnifiedSignup.storyboard b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/UnifiedSignup.storyboard deleted file mode 100644 index 2b0ec0487483..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/UnifiedSignup.storyboard +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/UnifiedSignupViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/UnifiedSignupViewController.swift deleted file mode 100644 index 694cd9644805..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/UnifiedSignupViewController.swift +++ /dev/null @@ -1,249 +0,0 @@ -import UIKit -import WordPressShared - -/// UnifiedSignupViewController: sign up to .com with an email address. -/// -class UnifiedSignupViewController: LoginViewController { - - /// Private properties. - /// - @IBOutlet private weak var tableView: UITableView! - - private var rows = [Row]() - private var errorMessage: String? - private var shouldChangeVoiceOverFocus: Bool = false - - // MARK: - Actions - @IBAction func handleContinueButtonTapped(_ sender: NUXButton) { - tracker.track(click: .requestMagicLink) - requestAuthenticationLink() - } - - // MARK: - View lifecycle - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.signUpTitle - - // Store default margin, and size table for the view. - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - - localizePrimaryButton() - registerTableViewCells() - loadRows() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - tracker.set(flow: .signup) - - if isMovingToParent { - tracker.track(step: .start) - } else { - tracker.set(step: .start) - } - } - - // MARK: - Overrides - - /// Style individual ViewController backgrounds, for now. - /// - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - /// Style individual ViewController status bars. - /// - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } - - /// Override the title on 'submit' button - /// - override func localizePrimaryButton() { - submitButton?.setTitle(WordPressAuthenticator.shared.displayStrings.magicLinkButtonTitle, for: .normal) - } - - /// Reload the tableview and show errors, if any. - /// - override func displayError(message: String, moveVoiceOverFocus: Bool = false) { - if errorMessage != message { - errorMessage = message - shouldChangeVoiceOverFocus = moveVoiceOverFocus - loadRows() - tableView.reloadData() - } - } -} - -// MARK: - UITableViewDataSource -extension UnifiedSignupViewController: UITableViewDataSource { - - /// Returns the number of rows in a section. - /// - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return rows.count - } - - /// Configure cells delegate method. - /// - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - - return cell - } -} - -// MARK: - UITableViewDelegate conformance -extension UnifiedSignupViewController: UITableViewDelegate { } - -// MARK: - Private methods -private extension UnifiedSignupViewController { - - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - GravatarEmailTableViewCell.reuseIdentifier: GravatarEmailTableViewCell.loadNib(), - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - } - - /// Describes how the tableView rows should be rendered. - /// - func loadRows() { - rows = [.gravatarEmail, .instructions] - - if let errorText = errorMessage, !errorText.isEmpty { - rows.append(.errorMessage) - } - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as GravatarEmailTableViewCell: - configureGravatarEmail(cell) - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextLabelTableViewCell where row == .errorMessage: - configureErrorLabel(cell) - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the gravatar + email cell. - /// - func configureGravatarEmail(_ cell: GravatarEmailTableViewCell) { - cell.configure(withEmail: loginFields.username) - } - - /// Configure the instruction cell. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.magicLinkSignupInstructions, style: .body) - } - - /// Configure the error message cell. - /// - func configureErrorLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: errorMessage, style: .error) - if shouldChangeVoiceOverFocus { - UIAccessibility.post(notification: .layoutChanged, argument: cell) - } - } - - // MARK: - Private Constants - - /// Rows listed in the order they were created. - /// - enum Row { - case gravatarEmail - case instructions - case errorMessage - - var reuseIdentifier: String { - switch self { - case .gravatarEmail: - return GravatarEmailTableViewCell.reuseIdentifier - case .instructions: - return TextLabelTableViewCell.reuseIdentifier - case .errorMessage: - return TextLabelTableViewCell.reuseIdentifier - } - } - } - - enum ErrorMessage: String { - case availabilityCheckFail = "availability_check_fail" - case magicLinkRequestFail = "magic_link_request_fail" - - func description() -> String { - switch self { - case .availabilityCheckFail: - return NSLocalizedString("Unable to verify the email address. Please try again later.", comment: "Error message displayed when an error occurred checking for email availability.") - case .magicLinkRequestFail: - return NSLocalizedString("We were unable to send you an email at this time. Please try again later.", comment: "Error message displayed when an error occurred sending the magic link email.") - } - } - } -} - -// MARK: - Instance Methods -/// Implementation methods imported from SignupEmailViewController. -/// -extension UnifiedSignupViewController { - // MARK: - Send email - - /// Makes the call to request a magic signup link be emailed to the user. - /// - func requestAuthenticationLink() { - loginFields.meta.emailMagicLinkSource = .signup - - configureSubmitButton(animating: true) - - let service = WordPressComAccountService() - service.requestSignupLink(for: loginFields.username, - success: { [weak self] in - self?.didRequestSignupLink() - self?.configureSubmitButton(animating: false) - }, failure: { [weak self] (error: Error) in - WPLogError("Request for signup link email failed.") - - guard let self else { - return - } - - self.tracker.track(failure: error.localizedDescription) - self.displayError(message: ErrorMessage.magicLinkRequestFail.description()) - self.configureSubmitButton(animating: false) - }) - } - - func didRequestSignupLink() { - guard let vc = SignupMagicLinkViewController.instantiate(from: .unifiedSignup) else { - WPLogError("Failed to navigate from UnifiedSignupViewController to SignupMagicLinkViewController") - return - } - - vc.loginFields = loginFields - vc.loginFields.restrictToWPCom = true - - navigationController?.pushViewController(vc, animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddress.storyboard b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddress.storyboard deleted file mode 100644 index daf2db63f942..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddress.storyboard +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewController+Extensions.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewController+Extensions.swift deleted file mode 100644 index 4fa95ab7c4af..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewController+Extensions.swift +++ /dev/null @@ -1,60 +0,0 @@ -import UIKit -import SwiftUI -import WordPressUI - -extension SiteAddressViewController { - static func showSiteAddressHelpAlert( - from presentingViewController: UIViewController, - sourceTag: WordPressSupportSourceTag, - moreHelpTapped: (() -> Void)? = nil, - onDismiss: (() -> Void)? = nil - ) { - let alert = AlertView { - AlertHeaderView( - title: Strings.title, - description: Strings.description - ) - } content: { - Image("site-address-illustration", bundle: WordPressAuthenticator.bundle) - .resizable() - .aspectRatio(contentMode: .fit) - .padding(.horizontal, 32) - .padding(.bottom, 32) - } actions: { - AlertDismissButton(onDismiss: onDismiss) - - Button(Strings.moreHelp) { - presentingViewController.presentedViewController?.dismiss(animated: true) { - guard WordPressAuthenticator.shared.delegate?.supportEnabled == true, - let viewController = UIApplication.shared.delegate?.window??.topmostPresentedViewController - else { - return - } - - moreHelpTapped?() - WordPressAuthenticator.shared.delegate?.presentSupportRequest(from: viewController, sourceTag: sourceTag) - } - } - } - - alert.present(in: presentingViewController) - } -} - -private enum Strings { - static let title = NSLocalizedString( - "login.siteAddressHelp.title", - value: "What's my site address?", - comment: "Title of alert helping users understand their site address during login" - ) - static let description = NSLocalizedString( - "login.siteAddressHelp.description", - value: "Your site address appears in the bar at the top of the screen when you visit your site in Safari.", - comment: "Description text explaining where to find site address during login" - ) - static let moreHelp = NSLocalizedString( - "login.siteAddressHelp.moreHelpButton", - value: "Need more help?", - comment: "Button title to get additional help finding site address during login" - ) -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewController.swift deleted file mode 100644 index 3e53f9d3f3f1..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewController.swift +++ /dev/null @@ -1,681 +0,0 @@ -import UIKit -import WordPressUI -import WordPressKit - -/// SiteAddressViewController: log in by Site Address. -/// -final class SiteAddressViewController: LoginViewController { - - /// Private properties. - /// - @IBOutlet private weak var tableView: UITableView! - @IBOutlet var bottomContentConstraint: NSLayoutConstraint? - - // Required for `NUXKeyboardResponder` but unused here. - var verticalCenterConstraint: NSLayoutConstraint? - - private var rows = [Row]() - private weak var siteURLField: UITextField? - private var errorMessage: String? - private var shouldChangeVoiceOverFocus: Bool = false - - /// A state variable that is `true` if network calls are currently happening and so the - /// view should be showing a loading indicator. - /// - /// This should only be modified within `configureViewLoading(_ loading:)`. - /// - /// This state is mainly used in `configureSubmitButton()` to determine whether the button - /// should show an activity indicator. - private var viewIsLoading: Bool = false - - /// Whether the protocol method `troubleshootSite` should be triggered after site info is fetched. - /// - private let isSiteDiscovery: Bool - private let configuration = WordPressAuthenticator.shared.configuration - private lazy var viewModel: SiteAddressViewModel = { - return SiteAddressViewModel( - isSiteDiscovery: isSiteDiscovery, - xmlrpcFacade: WordPressXMLRPCAPIFacade(), - authenticationDelegate: authenticationDelegate, - blogService: WordPressComBlogService(), - loginFields: loginFields - ) - }() - - init?(isSiteDiscovery: Bool, coder: NSCoder) { - self.isSiteDiscovery = isSiteDiscovery - super.init(coder: coder) - } - - required init?(coder: NSCoder) { - self.isSiteDiscovery = false - super.init(coder: coder) - } - - // MARK: - Actions - @IBAction func handleContinueButtonTapped(_ sender: NUXButton) { - tracker.track(click: .submit) - - validateForm() - } - - // MARK: - View lifecycle - override func viewDidLoad() { - super.viewDidLoad() - - removeGoogleWaitingView() - configureNavBar() - setupTable() - localizePrimaryButton() - registerTableViewCells() - loadRows() - configureSubmitButton() - configureForAccessibility() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - siteURLField?.text = loginFields.siteAddress - configureSubmitButton() - - // Nav bar could be hidden from the host app, so reshow it. - navigationController?.setNavigationBarHidden(false, animated: animated) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if isSiteDiscovery { - tracker.set(flow: .siteDiscovery) - } else { - tracker.set(flow: .loginWithSiteAddress) - } - - if isMovingToParent { - tracker.track(step: .start) - } else { - tracker.set(step: .start) - } - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - configureViewForEditingIfNeeded() - } - - // MARK: - Overrides - - override func styleBackground() { - view.backgroundColor = .systemBackground - } - - /// Style individual ViewController status bars. - /// - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } - - /// Configures the appearance and state of the submit button. - /// - /// Use this instead of the overridden `configureSubmitButton(animating:)` since this uses the - /// _current_ `viewIsLoading` state. - private func configureSubmitButton() { - configureSubmitButton(animating: viewIsLoading) - } - - /// Configures the appearance and state of the submit button. - /// - override func configureSubmitButton(animating: Bool) { - // This matches the string in WPiOS UI tests. - submitButton?.accessibilityIdentifier = "Site Address Next Button" - - submitButton?.showActivityIndicator(animating) - - submitButton?.isEnabled = ( - !animating && canSubmit() - ) - } - - /// Sets up accessibility elements in the order which they should be read aloud - /// and quiets repetitive elements. - /// - private func configureForAccessibility() { - view.accessibilityElements = [ - siteURLField as Any, - tableView as Any, - submitButton as Any - ] - - UIAccessibility.post(notification: .screenChanged, argument: siteURLField) - - if UIAccessibility.isVoiceOverRunning { - // Remove the placeholder if VoiceOver is running, because it speaks the label - // and the placeholder together. Since the placeholder matches the label, it's - // like VoiceOver is reading the same thing twice. - siteURLField?.placeholder = nil - } - } - - /// Sets the view's state to loading or not loading. - /// - /// - Parameter loading: True if the form should be configured to a "loading" state. - /// - override func configureViewLoading(_ loading: Bool) { - viewIsLoading = loading - - siteURLField?.isEnabled = !loading - - configureSubmitButton() - navigationItem.hidesBackButton = loading - } - - /// Configure the view for an editing state. Should only be called from viewWillAppear - /// as this method skips animating any change in height. - /// - @objc func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - siteURLField?.becomeFirstResponder() - } - } - - override func displayRemoteError(_ error: Error) { - guard authenticationDelegate.shouldHandleError(error) else { - super.displayRemoteError(error) - return - } - - authenticationDelegate.handleError(error) { customUI in - self.navigationController?.pushViewController(customUI, animated: true) - } - } - - /// Reload the tableview and show errors, if any. - /// - override func displayError(message: String, moveVoiceOverFocus: Bool = false) { - if errorMessage != message { - if !message.isEmpty { - tracker.track(failure: message) - } - - errorMessage = message - shouldChangeVoiceOverFocus = moveVoiceOverFocus - loadRows() - tableView.reloadData() - } - } -} - -// MARK: - UITableViewDataSource -extension SiteAddressViewController: UITableViewDataSource { - /// Returns the number of rows in a section. - /// - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return rows.count - } - - /// Configure cells delegate method. - /// - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - - return cell - } -} - -// MARK: - Keyboard Notifications -extension SiteAddressViewController: NUXKeyboardResponder { - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } -} - -// MARK: - TextField Delegate conformance -extension SiteAddressViewController: UITextFieldDelegate { - - /// Handle the keyboard `return` button action. - /// - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if canSubmit() { - validateForm() - return true - } - - return false - } -} - -// MARK: - Private methods -private extension SiteAddressViewController { - - // MARK: - Configuration - - func configureNavBar() { - navigationItem.title = WordPressAuthenticator.shared.displayStrings.logInTitle - } - - func setupTable() { - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - } - - // MARK: - Table Management - - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib(), - TextFieldTableViewCell.reuseIdentifier: TextFieldTableViewCell.loadNib(), - TextLinkButtonTableViewCell.reuseIdentifier: TextLinkButtonTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - } - - /// Describes how the tableView rows should be rendered. - /// - func loadRows() { - rows = [.instructions, .siteAddress] - - if let errorText = errorMessage, !errorText.isEmpty { - rows.append(.errorMessage) - } - - if WordPressAuthenticator.shared.configuration.displayHintButtons { - rows.append(.findSiteAddress) - } - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextFieldTableViewCell: - configureTextField(cell) - case let cell as TextLinkButtonTableViewCell: - configureTextLinkButton(cell) - case let cell as TextLabelTableViewCell where row == .errorMessage: - configureErrorLabel(cell) - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the instruction cell. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.siteLoginInstructions, style: .body) - } - - /// Configure the textfield cell. - /// - func configureTextField(_ cell: TextFieldTableViewCell) { - cell.configure(withStyle: .url, - placeholder: WordPressAuthenticator.shared.displayStrings.siteAddressPlaceholder) - - // Save a reference to the first textField so it can becomeFirstResponder. - siteURLField = cell.textField - cell.textField.delegate = self - cell.textField.text = loginFields.siteAddress - cell.onChangeSelectionHandler = { [weak self] textfield in - self?.loginFields.siteAddress = textfield.nonNilTrimmedText() - self?.configureSubmitButton() - } - - SigninEditingState.signinEditingStateActive = true - } - - /// Configure the "Find your site address" cell. - /// - func configureTextLinkButton(_ cell: TextLinkButtonTableViewCell) { - cell.configureButton(text: WordPressAuthenticator.shared.displayStrings.findSiteButtonTitle) - cell.actionHandler = { [weak self] in - guard let self else { - return - } - - self.tracker.track(click: .showHelp) - - SiteAddressViewController.showSiteAddressHelpAlert( - from: self, - sourceTag: self.sourceTag, - moreHelpTapped: { - self.tracker.track(click: .helpFindingSiteAddress) - }, - onDismiss: { - self.tracker.track(click: .dismiss) - - // Since we're showing an alert on top of this VC, `viewDidAppear` will not be called - // once the alert is dismissed (which is where the step would be reset automagically), - // so we need to manually reset the step here. - self.tracker.set(step: .start) - }) - - self.tracker.track(step: .help) - } - } - - /// Configure the error message cell. - /// - func configureErrorLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: errorMessage, style: .error) - if shouldChangeVoiceOverFocus { - UIAccessibility.post(notification: .layoutChanged, argument: cell) - } - } - - /// Push a custom view controller, provided by a host app, to the navigation stack - func pushCustomUI(_ customUI: UIViewController) { - /// Assign the help button of the newly injected UI to the same help button we are currently displaying - /// We are making a somewhat big assumption here: the chrome of the new UI we insert would look like the UI - /// WPAuthenticator is already displaying. Which is risky, but also kind of makes sense, considering - /// we are also pushing that injected UI to the current navigation controller. - if WordPressAuthenticator.shared.delegate?.supportActionEnabled == true { - customUI.navigationItem.rightBarButtonItems = self.navigationItem.rightBarButtonItems - } - - self.navigationController?.pushViewController(customUI, animated: true) - } - - // MARK: - Private Constants - - /// Rows listed in the order they were created. - /// - enum Row { - case instructions - case siteAddress - case findSiteAddress - case errorMessage - - var reuseIdentifier: String { - switch self { - case .instructions: - return TextLabelTableViewCell.reuseIdentifier - case .siteAddress: - return TextFieldTableViewCell.reuseIdentifier - case .findSiteAddress: - return TextLinkButtonTableViewCell.reuseIdentifier - case .errorMessage: - return TextLabelTableViewCell.reuseIdentifier - } - } - } -} - -// MARK: - Instance Methods - -private extension SiteAddressViewController { - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with the submit action. - /// - func validateForm() { - view.endEditing(true) - displayError(message: "") - - // We need to to this here because before this point we need the URL to be pre-validated - // exactly as the user inputs it, and after this point we need the URL to be the base site URL. - // This isn't really great, but it's the only sane solution I could come up with given the current - // architecture of this pod. - loginFields.siteAddress = WordPressAuthenticator.baseSiteURL(string: loginFields.siteAddress) - - configureViewLoading(true) - - guard let url = URL(string: loginFields.siteAddress) else { - configureViewLoading(false) - return displayError(message: Localization.invalidURL, moveVoiceOverFocus: true) - } - - // Checks that the site exists - checkSiteExistence(url: url) { [weak self] in - guard let self else { return } - // skips XMLRPC check for site discovery or site address login if needed - if (self.isSiteDiscovery && self.configuration.skipXMLRPCCheckForSiteDiscovery) || - self.configuration.skipXMLRPCCheckForSiteAddressLogin { - self.fetchSiteInfo() - return - } - // Proceeds to check for the site's WordPress - self.guessXMLRPCURL(for: url.absoluteString) - } - } - - func checkSiteExistence(url: URL, onCompletion: @escaping () -> Void) { - var request = URLRequest(url: url) - request.httpMethod = "GET" - request.timeoutInterval = 10.0 // waits for 10 seconds - let task = URLSession.shared.dataTask(with: request) { [weak self] _, _, error in - DispatchQueue.main.async { [weak self] in - guard let self else { return } - - if let error, (error as NSError).code != NSURLErrorAppTransportSecurityRequiresSecureConnection { - self.configureViewLoading(false) - - if self.authenticationDelegate.shouldHandleError(error) { - self.authenticationDelegate.handleError(error) { customUI in - self.pushCustomUI(customUI) - } - return - } - - var message: String? - - // Use `URLError`'s error message (which usually contains more accurate description), if the - // error is SSL error. - if let urlError = error as? URLError, urlError.failureURLPeerTrust != nil { - message = urlError.localizedDescription - } - - return self.displayError(message: message ?? Localization.nonExistentSiteError, moveVoiceOverFocus: true) - } - - onCompletion() - } - } - task.resume() - } - - func guessXMLRPCURL(for siteAddress: String) { - viewModel.guessXMLRPCURL( - for: siteAddress, - loading: { [weak self] isLoading in - self?.configureViewLoading(isLoading) - }, - completion: { [weak self] result -> Void in - guard let self else { return } - switch result { - case .success: - // Let's try to grab site info in preparation for the next screen. - self.fetchSiteInfo() - case .error(let error, let errorMessage): - if let message = errorMessage { - self.displayError(message: message, moveVoiceOverFocus: true) - } else { - self.displayError(error, sourceTag: self.sourceTag) - } - case .troubleshootSite: - WordPressAuthenticator.shared.delegate?.troubleshootSite(nil, in: self.navigationController) - case .customUI(let viewController): - self.pushCustomUI(viewController) - } - }) - } - - func fetchSiteInfo() { - let baseSiteUrl = WordPressAuthenticator.baseSiteURL(string: loginFields.siteAddress) - let service = WordPressComBlogService() - - let successBlock: (WordPressComSiteInfo) -> Void = { [weak self] siteInfo in - guard let self else { - return - } - self.configureViewLoading(false) - if siteInfo.isWPCom && WordPressAuthenticator.shared.delegate?.allowWPComLogin == false { - // Hey, you have to log out of your existing WP.com account before logging into another one. - self.promptUserToLogoutBeforeConnectingWPComSite() - return - } - self.presentNextControllerIfPossible(siteInfo: siteInfo) - } - - service.fetchUnauthenticatedSiteInfoForAddress(for: baseSiteUrl, success: successBlock, failure: { [weak self] error in - self?.configureViewLoading(false) - guard let self else { - return - } - - if self.authenticationDelegate.shouldHandleError(error) { - self.authenticationDelegate.handleError(error) { [weak self] customUI in - self?.navigationController?.pushViewController(customUI, animated: true) - } - } else { - self.displayError(message: Localization.invalidURL) - } - }) - } - - func presentNextControllerIfPossible(siteInfo: WordPressComSiteInfo?) { - - // Ensure that we're using the verified URL before passing the `loginFields` to the next - // view controller. - // - // In some scenarios, the text field change callback in `configureTextField()` gets executed - // right after we validated and modified `loginFields.siteAddress` in `validateForm()`. And - // this causes the value of `loginFields.siteAddress` to be reset to what the user entered. - // - // Using the user-entered `loginFields.siteAddress` causes problems when we try to log - // the user in especially if they just use a domain. For example, validating their - // self-hosted site credentials fails because the - // `WordPressOrgXMLRPCValidator.guessXMLRPCURLForSite` expects a complete site URL. - // - // This routine fixes that problem. We'll use what we already validated from - // `fetchSiteInfo()`. - // - if let verifiedSiteAddress = siteInfo?.url { - loginFields.siteAddress = verifiedSiteAddress - } - - guard isSiteDiscovery == false else { - WordPressAuthenticator.shared.delegate?.troubleshootSite(siteInfo, in: navigationController) - return - } - - guard siteInfo?.isWPCom == false else { - showGetStarted() - return - } - - WordPressAuthenticator.shared.delegate?.shouldPresentUsernamePasswordController(for: siteInfo, onCompletion: { result in - switch result { - case let .error(error): - self.displayError(message: error.localizedDescription) - case let .presentPasswordController(isSelfHosted): - if isSelfHosted { - self.showSelfHostedUsernamePassword() - return - } - - self.showWPUsernamePassword() - case .presentEmailController: - self.showGetStarted() - case let .injectViewController(customUI): - self.pushCustomUI(customUI) - } - }) - } - - func originalErrorOrError(error: NSError) -> NSError { - guard let err = error.userInfo[XMLRPCOriginalErrorKey] as? NSError else { - return error - } - - return err - } - - /// Here we will continue with the self-hosted flow. - /// - func showSelfHostedUsernamePassword() { - configureViewLoading(false) - guard let vc = SiteCredentialsViewController.instantiate(from: .siteAddress) else { - WPLogError("Failed to navigate from SiteAddressViewController to SiteCredentialsViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - /// Break away from the self-hosted flow. - /// Display a username / password login screen for WP.com sites. - /// - func showWPUsernamePassword() { - configureViewLoading(false) - - guard let vc = LoginUsernamePasswordViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from SiteAddressViewController to LoginUsernamePasswordViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - /// If the site is WordPressDotCom, redirect to WP login. - /// - func showGetStarted() { - guard let vc = GetStartedViewController.instantiate(from: .getStarted) else { - WPLogError("Failed to navigate from SiteAddressViewController to GetStartedViewController") - return - } - vc.source = .wpComSiteAddress - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - /// Whether the form can be submitted. - /// - func canSubmit() -> Bool { - return loginFields.validateSiteForSignin() - } - - @objc private func promptUserToLogoutBeforeConnectingWPComSite() { - let acceptActionTitle = NSLocalizedString("OK", comment: "Alert dismissal title") - let message = NSLocalizedString("Please log out before connecting to a different wordpress.com site", comment: "Message for alert to prompt user to logout before connecting to a different wordpress.com site.") - let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) - alertController.addDefaultActionWithTitle(acceptActionTitle) - present(alertController, animated: true) - } -} - -private extension SiteAddressViewController { - enum Localization { - static let invalidURL = NSLocalizedString( - "Invalid URL. Please double-check and try again.", - comment: "Error message shown when the input URL is invalid.") - static let nonExistentSiteError = NSLocalizedString( - "Cannot access the site at this address. Please double-check and try again.", - comment: "Error message shown when the input URL does not point to an existing site.") - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewModel.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewModel.swift deleted file mode 100644 index aa81e20a74c2..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewModel.swift +++ /dev/null @@ -1,136 +0,0 @@ -import Foundation -import WordPressKit -import WordPressShared - -struct SiteAddressViewModel { - private let isSiteDiscovery: Bool - private let xmlrpcFacade: WordPressXMLRPCAPIFacade - private unowned let authenticationDelegate: WordPressAuthenticatorDelegate - private let blogService: WordPressComBlogService - private var loginFields: LoginFields - - private let tracker = AuthenticatorAnalyticsTracker.shared - - init(isSiteDiscovery: Bool, - xmlrpcFacade: WordPressXMLRPCAPIFacade, - authenticationDelegate: WordPressAuthenticatorDelegate, - blogService: WordPressComBlogService, - loginFields: LoginFields - ) { - self.isSiteDiscovery = isSiteDiscovery - self.xmlrpcFacade = xmlrpcFacade - self.authenticationDelegate = authenticationDelegate - self.blogService = blogService - self.loginFields = loginFields - } - - enum GuessXMLRPCURLResult: Equatable { - case success - case error(NSError, String?) - case troubleshootSite - case customUI(UIViewController) - } - - func guessXMLRPCURL( - for siteAddress: String, - loading: @escaping ((Bool) -> ()), - completion: @escaping (GuessXMLRPCURLResult) -> () - ) { - xmlrpcFacade.guessXMLRPCURL(forSite: siteAddress, success: { url in - // Success! We now know that we have a valid XML-RPC endpoint. - // At this point, we do NOT know if this is a WP.com site or a self-hosted site. - if let url { - self.loginFields.meta.xmlrpcURL = url as NSURL - } - - completion(.success) - }, failure: { error in - guard let error else { - return - } - // Intentionally log the attempted address on failures. - // It's not guaranteed to be included in the error object depending on the error. - WPLogInfo("Error attempting to connect to site address: \(self.loginFields.siteAddress)") - WPLogError(error.localizedDescription) - - self.tracker.track(failure: .loginFailedToGuessXMLRPC) - - loading(false) - - guard self.isSiteDiscovery == false else { - completion(.troubleshootSite) - return - } - - let err = self.originalErrorOrError(error: error as NSError) - self.handleGuessXMLRPCURLError(error: err, loading: loading, completion: completion) - }) - } - - private func handleGuessXMLRPCURLError( - error: NSError, - loading: @escaping ((Bool) -> ()), - completion: @escaping (GuessXMLRPCURLResult) -> () - ) { - let completion: (NSError, String?) -> Void = { error, errorMessage in - if self.authenticationDelegate.shouldHandleError(error) { - self.authenticationDelegate.handleError(error) { customUI in - completion(.customUI(customUI)) - } - if let message = errorMessage { - self.tracker.track(failure: message) - } - return - } - - completion(.error(error, errorMessage)) - } - - /// Confirm the site is not a WordPress site before describing it as an invalid WP site - if let xmlrpcValidatorError = error as? WordPressOrgXMLRPCValidatorError, xmlrpcValidatorError == .invalid { - loading(true) - isWPSite { isWP in - loading(false) - if isWP { - let error = WordPressOrgXMLRPCValidatorError.xmlrpc_missing - completion(error as NSError, error.localizedDescription) - } else { - completion(error, Strings.notWPSiteErrorMessage) - } - } - } else if (error.domain == NSURLErrorDomain && error.code == NSURLErrorCannotFindHost) || - (error.domain == NSURLErrorDomain && error.code == NSURLErrorNetworkConnectionLost) { - completion(error, Strings.notWPSiteErrorMessage) - } else { - completion(error, (error as? WordPressOrgXMLRPCValidatorError)?.localizedDescription) - } - } - - private func originalErrorOrError(error: NSError) -> NSError { - guard let err = error.userInfo[XMLRPCOriginalErrorKey] as? NSError else { - return error - } - - return err - } -} - -extension SiteAddressViewModel { - private func isWPSite(_ completion: @escaping (Bool) -> ()) { - let baseSiteUrl = WordPressAuthenticator.baseSiteURL(string: loginFields.siteAddress) - blogService.fetchUnauthenticatedSiteInfoForAddress( - for: baseSiteUrl, - success: { siteInfo in - completion(siteInfo.isWP) - }, - failure: { _ in - completion(false) - }) - } -} - -private extension SiteAddressViewModel { - struct Strings { - static let notWPSiteErrorMessage = NSLocalizedString("The site at this address is not a WordPress site. For us to connect to it, the site must use WordPress.", comment: "Error message shown when a URL does not point to an existing site.") - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteCredentialsViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteCredentialsViewController.swift deleted file mode 100644 index 89b4d15d8eaa..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteCredentialsViewController.swift +++ /dev/null @@ -1,589 +0,0 @@ -import UIKit -import WordPressShared - -/// Part two of the self-hosted sign in flow: username + password. Used by WPiOS and NiOS. -/// A valid site address should be acquired before presenting this view controller. -/// -final class SiteCredentialsViewController: LoginViewController { - - /// Private properties. - /// - @IBOutlet private weak var tableView: UITableView! - @IBOutlet var bottomContentConstraint: NSLayoutConstraint? - - private weak var usernameField: UITextField? - private weak var passwordField: UITextField? - private var rows = [Row]() - private var errorMessage: String? - private var shouldChangeVoiceOverFocus: Bool = false - - private let isDismissible: Bool - private let completionHandler: ((WordPressOrgCredentials) -> Void)? - private let configuration = WordPressAuthenticator.shared.configuration - - init?(coder: NSCoder, isDismissible: Bool, onCompletion: @escaping (WordPressOrgCredentials) -> Void) { - self.isDismissible = isDismissible - self.completionHandler = onCompletion - super.init(coder: coder) - } - - required init?(coder: NSCoder) { - self.isDismissible = false - self.completionHandler = nil - super.init(coder: coder) - } - - // Required for `NUXKeyboardResponder` but unused here. - var verticalCenterConstraint: NSLayoutConstraint? - - override var sourceTag: WordPressSupportSourceTag { - get { - return .loginUsernamePassword - } - } - - override var loginFields: LoginFields { - didSet { - // Clear the password (if any) from LoginFields - loginFields.password = "" - } - } - - // MARK: - Actions - @IBAction func handleContinueButtonTapped(_ sender: NUXButton) { - tracker.track(click: .submit) - - validateForm() - } - - // MARK: - View lifecycle - override func viewDidLoad() { - super.viewDidLoad() - - loginFields.meta.userIsDotCom = false - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.logInTitle - if isDismissible { - navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(dismissView)) - } - - // Store default margin, and size table for the view. - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - - localizePrimaryButton() - registerTableViewCells() - loadRows() - configureForAccessibility() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if isMovingToParent { - tracker.track(step: .usernamePassword) - } else { - tracker.set(step: .usernamePassword) - } - - configureSubmitButton(animating: false) - configureViewLoading(false) - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - configureViewForEditingIfNeeded() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - } - - // MARK: - Overrides - - /// Style individual ViewController backgrounds, for now. - /// - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } - - /// Configures the appearance and state of the submit button. - /// - override func configureSubmitButton(animating: Bool) { - submitButton?.showActivityIndicator(animating) - - submitButton?.isEnabled = ( - !animating && - !loginFields.username.isEmpty && - !loginFields.password.isEmpty - ) - } - - /// Sets up accessibility elements in the order which they should be read aloud - /// and chooses which element to focus on at the beginning. - /// - private func configureForAccessibility() { - view.accessibilityElements = [ - usernameField as Any, - tableView as Any, - submitButton as Any - ] - - UIAccessibility.post(notification: .screenChanged, argument: usernameField) - } - - /// Sets the view's state to loading or not loading. - /// - /// - Parameter loading: True if the form should be configured to a "loading" state. - /// - override func configureViewLoading(_ loading: Bool) { - usernameField?.isEnabled = !loading - passwordField?.isEnabled = !loading - - configureSubmitButton(animating: loading) - navigationItem.hidesBackButton = loading - } - - /// Set error messages and reload the table to display them. - /// - override func displayError(message: String, moveVoiceOverFocus: Bool = false) { - if errorMessage != message { - if !message.isEmpty { - tracker.track(failure: message) - } - - errorMessage = message - shouldChangeVoiceOverFocus = moveVoiceOverFocus - loadRows() - tableView.reloadData() - } - } - - /// No-op. Required by LoginFacade. - func displayLoginMessage(_ message: String) {} -} - -// MARK: - UITableViewDataSource -extension SiteCredentialsViewController: UITableViewDataSource { - /// Returns the number of rows in a section. - /// - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return rows.count - } - - /// Configure cells delegate method. - /// - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - - return cell - } -} - -// MARK: - UITableViewDelegate conformance -extension SiteCredentialsViewController: UITableViewDelegate { - /// After a textfield cell is done displaying, remove the textfield reference. - /// - func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { - guard let row = rows[safe: indexPath.row] else { - return - } - - if row == .username { - usernameField = nil - } else if row == .password { - passwordField = nil - } - } -} - -// MARK: - Keyboard Notifications -extension SiteCredentialsViewController: NUXKeyboardResponder { - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } -} - -// MARK: - TextField Delegate conformance -extension SiteCredentialsViewController: UITextFieldDelegate { - - /// Handle the keyboard `return` button action. - /// - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if textField == usernameField { - if UIAccessibility.isVoiceOverRunning { - passwordField?.placeholder = nil - } - passwordField?.becomeFirstResponder() - } else if textField == passwordField { - validateForm() - } - return true - } -} - -// MARK: - Private Methods -private extension SiteCredentialsViewController { - - @objc func dismissView() { - dismissBlock?(true) - } - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib(), - TextFieldTableViewCell.reuseIdentifier: TextFieldTableViewCell.loadNib(), - TextLinkButtonTableViewCell.reuseIdentifier: TextLinkButtonTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - } - - /// Describes how the tableView rows should be rendered. - /// - func loadRows() { - rows = [.instructions, .username, .password] - - if let errorText = errorMessage, !errorText.isEmpty { - rows.append(.errorMessage) - } - - if configuration.displayHintButtons { - rows.append(.forgotPassword) - } - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextFieldTableViewCell where row == .username: - storeUsernameTextField(cell) - case let cell as TextFieldTableViewCell where row == .password: - configurePasswordTextField(cell) - case let cell as TextLinkButtonTableViewCell: - configureForgotPassword(cell) - case let cell as TextLabelTableViewCell where row == .errorMessage: - configureErrorLabel(cell) - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the instruction cell. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - let displayURL = sanitizedSiteAddress(siteAddress: loginFields.siteAddress) - let text = String.localizedStringWithFormat(WordPressAuthenticator.shared.displayStrings.siteCredentialInstructions, displayURL) - cell.configureLabel(text: text, style: .body) - } - - /// Configure the username textfield cell. - /// - func storeUsernameTextField(_ cell: TextFieldTableViewCell) { - cell.configure(withStyle: .username, - placeholder: WordPressAuthenticator.shared.displayStrings.usernamePlaceholder, - text: loginFields.username) - - // Save a reference to the textField so it can becomeFirstResponder. - usernameField = cell.textField - cell.textField.delegate = self - - cell.onChangeSelectionHandler = { [weak self] textfield in - self?.loginFields.username = textfield.nonNilTrimmedText() - self?.configureSubmitButton(animating: false) - } - - SigninEditingState.signinEditingStateActive = true - if UIAccessibility.isVoiceOverRunning { - // Quiet repetitive elements in VoiceOver. - usernameField?.placeholder = nil - } - } - - /// Configure the password textfield cell. - /// - func configurePasswordTextField(_ cell: TextFieldTableViewCell) { - cell.configure(withStyle: .password, - placeholder: WordPressAuthenticator.shared.displayStrings.passwordPlaceholder, - text: loginFields.password) - passwordField = cell.textField - cell.textField.delegate = self - cell.onChangeSelectionHandler = { [weak self] textfield in - self?.loginFields.password = textfield.nonNilTrimmedText() - self?.configureSubmitButton(animating: false) - } - - if UIAccessibility.isVoiceOverRunning { - // Quiet repetitive elements in VoiceOver. - passwordField?.placeholder = nil - } - } - - /// Configure the forgot password cell. - /// - func configureForgotPassword(_ cell: TextLinkButtonTableViewCell) { - cell.configureButton(text: WordPressAuthenticator.shared.displayStrings.resetPasswordButtonTitle, accessibilityTrait: .link) - cell.actionHandler = { [weak self] in - guard let self else { - return - } - - self.tracker.track(click: .forgottenPassword) - - // If information is currently processing, ignore button tap. - guard self.enableSubmit(animating: false) else { - return - } - - WordPressAuthenticator.openForgotPasswordURL(self.loginFields) - } - } - - /// Configure the error message cell. - /// - func configureErrorLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: errorMessage, style: .error) - if shouldChangeVoiceOverFocus { - UIAccessibility.post(notification: .layoutChanged, argument: cell) - } - } - - /// Configure the view for an editing state. - /// - func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - usernameField?.becomeFirstResponder() - } - } - - /// Presents verify email instructions screen - /// - /// - Parameters: - /// - loginFields: `LoginFields` instance created using `makeLoginFieldsUsing` helper method - /// - func presentVerifyEmail(loginFields: LoginFields) { - guard let vc = VerifyEmailViewController.instantiate(from: .verifyEmail) else { - WPLogError("Failed to navigate from SiteCredentialsViewController to VerifyEmailViewController") - return - } - - vc.loginFields = loginFields - navigationController?.pushViewController(vc, animated: true) - } - - /// Used for creating `LoginFields` - /// - /// - Parameters: - /// - xmlrpc: XML-RPC URL as a String - /// - options: Dictionary received from .org site credential authentication response. (Containing `jetpack_user_email` and `home_url` values) - /// - /// - Returns: A valid `LoginFields` instance or `nil` - /// - func makeLoginFieldsUsing(xmlrpc: String, options: [AnyHashable: Any]) -> LoginFields? { - guard let xmlrpcURL = URL(string: xmlrpc) else { - WPLogError("Failed to initiate XML-RPC URL from \(xmlrpc)") - return nil - } - - // `jetpack_user_email` to be used for WPCOM login - guard let email = options["jetpack_user_email"] as? [String: Any], - let userName = email["value"] as? String else { - WPLogError("Failed to find jetpack_user_email value.") - return nil - } - - // Site address - guard let home_url = options["home_url"] as? [String: Any], - let siteAddress = home_url["value"] as? String else { - WPLogError("Failed to find home_url value.") - return nil - } - - let loginFields = LoginFields() - loginFields.meta.xmlrpcURL = xmlrpcURL as NSURL - loginFields.username = userName - loginFields.siteAddress = siteAddress - return loginFields - } - - func validateFormAndTriggerDelegate() { - view.endEditing(true) - displayError(message: "") - - // Is everything filled out? - if !loginFields.validateFieldsPopulatedForSignin() { - let errorMsg = NSLocalizedString("Please fill out all the fields", - comment: "A short prompt asking the user to properly fill out all login fields.") - displayError(message: errorMsg) - - return - } - - configureViewLoading(true) - - guard let delegate = WordPressAuthenticator.shared.delegate else { - fatalError("Error: Where did the delegate go?") - } - // manually construct the XMLRPC since this is needed to get the site address later - let xmlrpc = loginFields.siteAddress + "/xmlrpc.php" - let wporg = WordPressOrgCredentials(username: loginFields.username, - password: loginFields.password, - xmlrpc: xmlrpc, - options: [:]) - delegate.handleSiteCredentialLogin(credentials: wporg, onLoading: { [weak self] shouldShowLoading in - self?.configureViewLoading(shouldShowLoading) - }, onSuccess: { [weak self] in - self?.finishedLogin(withUsername: wporg.username, - password: wporg.password, - xmlrpc: wporg.xmlrpc, - options: wporg.options) - }, onFailure: { [weak self] error, incorrectCredentials in - self?.handleLoginFailure(error: error, incorrectCredentials: incorrectCredentials) - }) - } - - func handleLoginFailure(error: Error, incorrectCredentials: Bool) { - configureViewLoading(false) - guard configuration.enableManualErrorHandlingForSiteCredentialLogin == false else { - WordPressAuthenticator.shared.delegate?.handleSiteCredentialLoginFailure(error: error, for: loginFields.siteAddress, in: self) - return - } - if incorrectCredentials { - let message = NSLocalizedString("It looks like this username/password isn't associated with this site.", - comment: "An error message shown during log in when the username or password is incorrect.") - displayError(message: message, moveVoiceOverFocus: true) - } else { - displayError(error, sourceTag: sourceTag) - } - } - - func syncDataOrPresentWPComLogin(with wporgCredentials: WordPressOrgCredentials) { - if configuration.isWPComLoginRequiredForSiteCredentialsLogin { - presentWPComLogin(wporgCredentials: wporgCredentials) - return - } - // Client didn't explicitly ask for WPCOM credentials. (`isWPComLoginRequiredForSiteCredentialsLogin` is false) - // So, sync the available credentials and finish sign in. - // - let credentials = AuthenticatorCredentials(wporg: wporgCredentials) - WordPressAuthenticator.shared.delegate?.sync(credentials: credentials) { [weak self] in - NotificationCenter.default.post(name: Foundation.Notification.Name(rawValue: WordPressAuthenticator.WPSigninDidFinishNotification), object: nil) - self?.showLoginEpilogue(for: credentials) - } - } - - func presentWPComLogin(wporgCredentials: WordPressOrgCredentials) { - // Try to get the jetpack email from XML-RPC response dictionary. - // - guard let loginFields = makeLoginFieldsUsing(xmlrpc: wporgCredentials.xmlrpc, - options: wporgCredentials.options) else { - WPLogError("Unexpected response from .org site credentials sign in using XMLRPC.") - let credentials = AuthenticatorCredentials(wporg: wporgCredentials) - showLoginEpilogue(for: credentials) - return - } - - // Present verify email instructions screen. Passing loginFields will prefill the jetpack email in `VerifyEmailViewController` - // - presentVerifyEmail(loginFields: loginFields) - } - - // MARK: - Private Constants - - /// Rows listed in the order they were created. - /// - enum Row { - case instructions - case username - case password - case forgotPassword - case errorMessage - - var reuseIdentifier: String { - switch self { - case .instructions: - return TextLabelTableViewCell.reuseIdentifier - case .username: - return TextFieldTableViewCell.reuseIdentifier - case .password: - return TextFieldTableViewCell.reuseIdentifier - case .forgotPassword: - return TextLinkButtonTableViewCell.reuseIdentifier - case .errorMessage: - return TextLabelTableViewCell.reuseIdentifier - } - } - } -} - -// MARK: - Instance Methods -/// Implementation methods copied from LoginSelfHostedViewController. -/// -extension SiteCredentialsViewController { - /// Sanitize and format the site address we show to users. - /// - @objc func sanitizedSiteAddress(siteAddress: String) -> String { - let baseSiteUrl = WordPressAuthenticator.baseSiteURL(string: siteAddress) as NSString - if let str = baseSiteUrl.components(separatedBy: "://").last { - return str - } - return siteAddress - } - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with the submit action. - /// - @objc func validateForm() { - guard configuration.enableManualSiteCredentialLogin else { - return validateFormAndLogin() // handles login with XMLRPC normally - } - - // asks the delegate to handle the login - validateFormAndTriggerDelegate() - } - - func finishedLogin(withUsername username: String, password: String, xmlrpc: String, options: [AnyHashable: Any]) { - let wporg = WordPressOrgCredentials(username: username, password: password, xmlrpc: xmlrpc, options: options) - /// If `completionHandler` is available, return early with the credentials. - if let completionHandler { - completionHandler(wporg) - } else { - syncDataOrPresentWPComLogin(with: wporg) - } - } - - override func displayRemoteError(_ error: Error) { - configureViewLoading(false) - let err = error as NSError - if err.code == 403 { - let message = NSLocalizedString("It looks like this username/password isn't associated with this site.", - comment: "An error message shown during log in when the username or password is incorrect.") - displayError(message: message, moveVoiceOverFocus: true) - } else { - displayError(error, sourceTag: sourceTag) - } - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/VerifyEmail/VerifyEmail.storyboard b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/VerifyEmail/VerifyEmail.storyboard deleted file mode 100644 index d844a3f5afe5..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/VerifyEmail/VerifyEmail.storyboard +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/VerifyEmail/VerifyEmailViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/VerifyEmail/VerifyEmailViewController.swift deleted file mode 100644 index 3c61ccc7cfa6..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/VerifyEmail/VerifyEmailViewController.swift +++ /dev/null @@ -1,258 +0,0 @@ -import UIKit -import WordPressShared - -final class VerifyEmailViewController: LoginViewController { - - // MARK: - Properties - - @IBOutlet private weak var tableView: UITableView! - private var buttonViewController: NUXButtonViewController? - private let rows = Row.allCases - - // MARK: - View lifecycle - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.logInTitle - - // Store default margin, and size table for the view. - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - - registerTableViewCells() - configureButtonViewController() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if isBeingPresentedInAnyWay { - tracker.track(step: .verifyEmailInstructions) - } else { - tracker.set(step: .verifyEmailInstructions) - } - } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - super.prepare(for: segue, sender: sender) - - if let vc = segue.destination as? NUXButtonViewController { - buttonViewController = vc - } - } - - // MARK: - Overrides - - override var sourceTag: WordPressSupportSourceTag { - .verifyEmailInstructions - } - - /// Style individual ViewController backgrounds, for now. - /// - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - return super.styleBackground() - } - - view.backgroundColor = unifiedBackgroundColor - } - - /// Style individual ViewController status bars. - /// - override var preferredStatusBarStyle: UIStatusBarStyle { - WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } - - /// Customise loading state of view. - /// - override func configureViewLoading(_ loading: Bool) { - buttonViewController?.setTopButtonState(isLoading: loading, - isEnabled: !loading) - buttonViewController?.setBottomButtonState(isLoading: false, - isEnabled: !loading) - navigationItem.hidesBackButton = loading - } -} - -// MARK: - UITableViewDataSource -extension VerifyEmailViewController: UITableViewDataSource { - /// Returns the number of rows in a section. - /// - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - rows.count - } - - /// Configure cells delegate method. - /// - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - - return cell - } -} - -// MARK: - Private Methods -private extension VerifyEmailViewController { - /// Configure bottom buttons. - /// - func configureButtonViewController() { - guard let buttonViewController else { - return - } - - buttonViewController.hideShadowView() - - // Setup `Send email verification link` button - buttonViewController.setupTopButton(title: ButtonConfiguration.SendEmailVerificationLink.title, - isPrimary: true) { [weak self] in - self?.handleSendEmailVerificationLinkButtonTapped() - } - - // Setup `Login with account password` button - buttonViewController.setupBottomButton(title: ButtonConfiguration.LoginWithAccountPassword.title, - isPrimary: false) { [weak self] in - self?.handleLoginWithAccountPasswordButtonTapped() - } - } - - // MARK: - Actions - @objc func handleSendEmailVerificationLinkButtonTapped() { - tracker.track(click: .requestMagicLink) - requestAuthenticationLink() - } - - @objc func handleLoginWithAccountPasswordButtonTapped() { - tracker.track(click: .loginWithAccountPassword) - presentUnifiedPassword() - } - - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - GravatarEmailTableViewCell.reuseIdentifier: GravatarEmailTableViewCell.loadNib(), - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as GravatarEmailTableViewCell where row == .persona: - configureGravatarEmail(cell) - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextLabelTableViewCell where row == .typePassword: - configureTypePasswordButton(cell) - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the gravatar + email cell. - /// - func configureGravatarEmail(_ cell: GravatarEmailTableViewCell) { - cell.configure(withEmail: loginFields.username) - } - - /// Configure the instructions cell. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.verifyMailLoginInstructions, - style: .body) - } - - /// Configure the enter password instructions cell. - /// - func configureTypePasswordButton(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.alternativelyEnterPasswordInstructions, - style: .body) - } - - /// Makes the call to request a magic authentication link be emailed to the user. - /// - func requestAuthenticationLink() { - loginFields.meta.emailMagicLinkSource = .login - - let email = loginFields.username - - configureViewLoading(true) - let service = WordPressComAccountService() - service.requestAuthenticationLink(for: email, - jetpackLogin: loginFields.meta.jetpackLogin, - success: { [weak self] in - self?.didRequestAuthenticationLink() - self?.configureViewLoading(false) - }, failure: { [weak self] (error: Error) in - guard let self else { return } - - self.tracker.track(failure: error.localizedDescription) - - self.displayError(error, sourceTag: self.sourceTag) - self.configureViewLoading(false) - }) - } - - /// When a magic link successfully sends, navigate the user to the next step. - /// - func didRequestAuthenticationLink() { - guard let vc = LoginMagicLinkViewController.instantiate(from: .unifiedLoginMagicLink) else { - WPLogError("Failed to navigate to LoginMagicLinkViewController from VerifyEmailViewController") - return - } - - vc.loginFields = loginFields - vc.loginFields.restrictToWPCom = true - navigationController?.pushViewController(vc, animated: true) - } - - /// Presents unified password screen - /// - func presentUnifiedPassword() { - guard let vc = PasswordViewController.instantiate(from: .password) else { - WPLogError("Failed to navigate to PasswordViewController from VerifyEmailViewController") - return - } - vc.loginFields = loginFields - navigationController?.pushViewController(vc, animated: true) - } - - // MARK: - Private Constants - - /// Rows listed in the order they were created. - /// - enum Row: CaseIterable { - case persona - case instructions - case typePassword - - var reuseIdentifier: String { - switch self { - case .persona: - return GravatarEmailTableViewCell.reuseIdentifier - case .instructions, .typePassword: - return TextLabelTableViewCell.reuseIdentifier - } - } - } -} - -// MARK: - Button configuration -private extension VerifyEmailViewController { - enum ButtonConfiguration { - enum SendEmailVerificationLink { - static let title = WordPressAuthenticator.shared.displayStrings.sendEmailVerificationLinkButtonTitle - } - - enum LoginWithAccountPassword { - static let title = WordPressAuthenticator.shared.displayStrings.loginWithAccountPasswordButtonTitle - } - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/WordPressComAccountService.swift b/Sources/WordPressAuthenticator/Helpers/WordPressComAccountService.swift deleted file mode 100644 index c1c62127079c..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/WordPressComAccountService.swift +++ /dev/null @@ -1,103 +0,0 @@ -import Foundation -import WordPressKit - -// MARK: - WordPressComAccountService -// -public class WordPressComAccountService { - - /// Makes the initializer public for external access. - public init() {} - - /// Indicates if a WordPress.com account is "PasswordLess": This kind of account must be authenticated via a Magic Link. - /// - public func isPasswordlessAccount(username: String, success: @escaping (Bool) -> Void, failure: @escaping (Error) -> Void) { - let remote = AccountServiceRemoteREST(wordPressComRestApi: anonymousAPI) - - remote.isPasswordlessAccount(username, success: { isPasswordless in - success(isPasswordless) - }, failure: { error in - let result = error ?? ServiceError.unknown - failure(result) - }) - } - - /// Connects a WordPress.com account with the specified Social Service. - /// - func connect(wpcomAuthToken: String, - serviceName: SocialServiceName, - serviceToken: String, - connectParameters: [String: AnyObject]? = nil, - success: @escaping () -> Void, - failure: @escaping (Error) -> Void) { - let loggedAPI = WordPressComRestApi(oAuthToken: wpcomAuthToken, - userAgent: configuration.userAgent, - baseURL: configuration.wpcomAPIBaseURL) - let remote = AccountServiceRemoteREST(wordPressComRestApi: loggedAPI) - - remote.connectToSocialService(serviceName, - serviceIDToken: serviceToken, - connectParameters: connectParameters, - oAuthClientID: configuration.wpcomClientId, - oAuthClientSecret: configuration.wpcomSecret, - success: success, - failure: { error in - failure(error) - }) - } - - /// Requests a WordPress.com Authentication Link to be sent to the specified email address. - /// - public func requestAuthenticationLink(for email: String, jetpackLogin: Bool, success: @escaping () -> Void, failure: @escaping (Error) -> Void) { - let remote = AccountServiceRemoteREST(wordPressComRestApi: anonymousAPI) - - remote.requestWPComAuthLink(forEmail: email, - clientID: configuration.wpcomClientId, - clientSecret: configuration.wpcomSecret, - source: jetpackLogin ? .jetpackConnect : .default, - wpcomScheme: configuration.wpcomScheme, - success: success, - failure: { error in - let result = error ?? ServiceError.unknown - failure(result) - }) - } - - /// Requests a WordPress.com SignUp Link to be sent to the specified email address. - /// - func requestSignupLink(for email: String, success: @escaping () -> Void, failure: @escaping (Error) -> Void) { - let remote = AccountServiceRemoteREST(wordPressComRestApi: anonymousAPI) - - remote.requestWPComSignupLink(forEmail: email, - clientID: configuration.wpcomClientId, - clientSecret: configuration.wpcomSecret, - wpcomScheme: configuration.wpcomScheme, - success: success, - failure: { error in - let result = error ?? ServiceError.unknown - failure(result) - }) - } - - /// Returns an anonymous WordPressComRestApi Instance. - /// - private var anonymousAPI: WordPressComRestApi { - return WordPressComRestApi(oAuthToken: nil, - userAgent: configuration.userAgent, - baseURL: configuration.wpcomAPIBaseURL) - } - - /// Returns the current WordPressAuthenticatorConfiguration Instance. - /// - private var configuration: WordPressAuthenticatorConfiguration { - return WordPressAuthenticator.shared.configuration - } -} - -// MARK: - Nested Types -// -extension WordPressComAccountService { - - enum ServiceError: Error { - case unknown - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/WordPressComBlogService.swift b/Sources/WordPressAuthenticator/Helpers/WordPressComBlogService.swift deleted file mode 100644 index 4c5e51842471..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/WordPressComBlogService.swift +++ /dev/null @@ -1,67 +0,0 @@ -import Foundation -import WordPressKit - -// MARK: - WordPress.com BlogService -// -class WordPressComBlogService { - - /// Returns a new anonymous instance of WordPressComRestApi. - /// - private var anonymousAPI: WordPressComRestApi { - let userAgent = WordPressAuthenticator.shared.configuration.userAgent - let baseUrl = WordPressAuthenticator.shared.configuration.wpcomAPIBaseURL - return WordPressComRestApi(oAuthToken: nil, userAgent: userAgent, baseURL: baseUrl, useEphemeralSession: true) - } - - /// Retrieves the WordPressComSiteInfo instance associated to a WordPress.com Site Address. - /// - func fetchSiteInfo(for address: String, success: @escaping (WordPressComSiteInfo) -> Void, failure: @escaping (Error) -> Void) { - let remote = BlogServiceRemoteREST(wordPressComRestApi: anonymousAPI, siteID: 0) - - remote.fetchSiteInfo(forAddress: address, success: { response in - guard let response else { - failure(ServiceError.unknown) - return - } - - let site = WordPressComSiteInfo(remote: response) - success(site) - }, failure: { error in - let result = error ?? ServiceError.unknown - failure(result) - }) - } - - func fetchUnauthenticatedSiteInfoForAddress(for address: String, success: @escaping (WordPressComSiteInfo) -> Void, failure: @escaping (Error) -> Void) { - let remote = BlogServiceRemoteREST(wordPressComRestApi: anonymousAPI, siteID: 0) - remote.fetchUnauthenticatedSiteInfo(forAddress: address, success: { response in - guard let response else { - failure(ServiceError.unknown) - return - } - - let site = WordPressComSiteInfo(remote: response) - guard site.url != Constants.wordPressBlogURL else { - failure(ServiceError.invalidWordPressAddress) - return - } - success(site) - }, failure: { error in - let result = error ?? ServiceError.unknown - failure(result) - }) - } -} - -// MARK: - Nested Types -// -extension WordPressComBlogService { - enum Constants { - static let wordPressBlogURL = "https://wordpress.com/blog" - } - - enum ServiceError: Error { - case unknown - case invalidWordPressAddress - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/WordPressComOAuthClientFacade.swift b/Sources/WordPressAuthenticator/Helpers/WordPressComOAuthClientFacade.swift deleted file mode 100644 index 579ec5abc957..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/WordPressComOAuthClientFacade.swift +++ /dev/null @@ -1,120 +0,0 @@ -import Foundation -import WordPressKit - -@objc public class WordPressComOAuthClientFacade: NSObject, WordPressComOAuthClientFacadeProtocol { - - private let client: WordPressComOAuthClient - - @objc required public init(client: String, secret: String) { - self.client = WordPressComOAuthClient( - clientID: client, - secret: secret, - wordPressComBaseURL: WordPressAuthenticator.shared.configuration.wpcomBaseURL, - wordPressComApiBaseURL: WordPressAuthenticator.shared.configuration.wpcomAPIBaseURL - ) - } - - public func authenticate( - username: String, - password: String, - multifactorCode: String?, - success: @escaping (_ authToken: String?) -> Void, - needsMultifactor: @escaping ((_ userID: Int, _ nonceInfo: SocialLogin2FANonceInfo?) -> Void), - failure: ((_ error: Error) -> Void)? - ) { - self.client.authenticate(username: username, password: password, multifactorCode: multifactorCode, needsMultifactor: needsMultifactor, success: success, failure: { error in - if case let .endpointError(authenticationFailure) = error, authenticationFailure.kind == .needsMultifactorCode { - needsMultifactor(0, nil) - } else { - failure?(error) - } - }) - } - - public func requestOneTimeCode( - username: String, - password: String, - success: @escaping () -> Void, - failure: @escaping (_ error: Error) -> Void - ) { - self.client.requestOneTimeCode(username: username, password: password, success: success, failure: failure) - } - - public func requestSocial2FACode( - userID: Int, - nonce: String, - success: @escaping (_ newNonce: String) -> Void, - failure: @escaping (_ error: Error, _ newNonce: String?) -> Void - ) { - self.client.requestSocial2FACode(userID: userID, nonce: nonce, success: success, failure: failure) - } - - public func authenticate( - socialIDToken: String, - service: String, - success: @escaping (_ authToken: String?) -> Void, - needsMultifactor: @escaping (_ userID: Int, _ nonceInfo: SocialLogin2FANonceInfo) -> Void, - existingUserNeedsConnection: @escaping (_ email: String) -> Void, - failure: @escaping (_ error: Error) -> Void - ) { - self.client.authenticate( - socialIDToken: socialIDToken, - service: service, - success: success, - needsMultifactor: needsMultifactor, - existingUserNeedsConnection: existingUserNeedsConnection, - failure: failure - ) - } - - public func authenticate( - socialLoginUser userID: Int, - authType: String, - twoStepCode: String, - twoStepNonce: String, - success: @escaping (_ authToken: String?) -> Void, - failure: @escaping (_ error: Error) -> Void - ) { - self.client.authenticate( - socialLoginUserID: userID, - authType: authType, - twoStepCode: twoStepCode, - twoStepNonce: twoStepNonce, - success: success, - failure: failure - ) - } - - public func requestWebauthnChallenge( - userID: Int64, - twoStepNonce: String, - success: @escaping (_ challengeData: WebauthnChallengeInfo) -> Void, - failure: @escaping (_ error: Error) -> Void - ) { - self.client.requestWebauthnChallenge(userID: userID, twoStepNonce: twoStepNonce, success: success, failure: failure) - } - - public func authenticateWebauthnSignature( - userID: Int64, - twoStepNonce: String, - credentialID: Data, - clientDataJson: Data, - authenticatorData: Data, - signature: Data, - userHandle: Data, - success: @escaping (_ authToken: String) -> Void, - failure: @escaping (_ error: Error) -> Void - ) { - self.client.authenticateWebauthnSignature( - userID: userID, - twoStepNonce: twoStepNonce, - credentialID: credentialID, - clientDataJson: clientDataJson, - authenticatorData: authenticatorData, - signature: signature, - userHandle: userHandle, - success: success, - failure: failure - ) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/WordPressComOAuthClientFacadeProtocol.swift b/Sources/WordPressAuthenticator/Helpers/WordPressComOAuthClientFacadeProtocol.swift deleted file mode 100644 index 7abd37d69fc5..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/WordPressComOAuthClientFacadeProtocol.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Foundation - -import WordPressKit - -@objc public protocol WordPressComOAuthClientFacadeProtocol { - - init(client: String, secret: String) - - func authenticate( - username: String, - password: String, - multifactorCode: String?, - success: @escaping (_ authToken: String?) -> Void, - needsMultifactor: @escaping ((_ userID: Int, _ nonceInfo: SocialLogin2FANonceInfo?) -> Void), - failure: ((_ error: Error) -> Void)? - ) - - func requestOneTimeCode( - username: String, - password: String, - success: @escaping () -> Void, - failure: @escaping (_ error: Error) -> Void - ) - - func requestSocial2FACode( - userID: Int, - nonce: String, - success: @escaping (_ newNonce: String) -> Void, - failure: @escaping (_ error: Error, _ newNonce: String?) -> Void - ) - - func authenticate( - socialIDToken: String, - service: String, - success: @escaping (_ authToken: String?) -> Void, - needsMultifactor: @escaping (_ userID: Int, _ nonceInfo: SocialLogin2FANonceInfo) -> Void, - existingUserNeedsConnection: @escaping (_ email: String) -> Void, - failure: @escaping (_ error: Error) -> Void - ) - - func authenticate( - socialLoginUser userID: Int, - authType: String, - twoStepCode: String, - twoStepNonce: String, - success: @escaping (_ authToken: String?) -> Void, - failure: @escaping (_ error: Error) -> Void - ) - - func requestWebauthnChallenge( - userID: Int64, - twoStepNonce: String, - success: @escaping (_ challengeData: WebauthnChallengeInfo) -> Void, - failure: @escaping (_ error: Error) -> Void - ) - - func authenticateWebauthnSignature( - userID: Int64, - twoStepNonce: String, - credentialID: Data, - clientDataJson: Data, - authenticatorData: Data, - signature: Data, - userHandle: Data, - success: @escaping (_ authToken: String) -> Void, - failure: @escaping (_ error: Error) -> Void - ) -} diff --git a/Sources/WordPressAuthenticator/Helpers/WordPressXMLRPCAPIFacade.h b/Sources/WordPressAuthenticator/Helpers/WordPressXMLRPCAPIFacade.h deleted file mode 100644 index f1689a3c2280..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/WordPressXMLRPCAPIFacade.h +++ /dev/null @@ -1,23 +0,0 @@ -#import - -@protocol WordPressXMLRPCAPIFacade - -extern NSString *const XMLRPCOriginalErrorKey; - -- (instancetype)initWithUserAgent:(NSString *)userAgent; - -- (void)guessXMLRPCURLForSite:(NSString *)url - success:(void (^)(NSURL *xmlrpcURL))success - failure:(void (^)(NSError *error))failure; - -- (void)getBlogOptionsWithEndpoint:(NSURL *)xmlrpc - username:(NSString *)username - password:(NSString *)password - success:(void (^)(NSDictionary *options))success - failure:(void (^)(NSError *error))failure; - -@end - -@interface WordPressXMLRPCAPIFacade : NSObject - -@end diff --git a/Sources/WordPressAuthenticator/Helpers/WordPressXMLRPCAPIFacade.m b/Sources/WordPressAuthenticator/Helpers/WordPressXMLRPCAPIFacade.m deleted file mode 100644 index 0fcce7f6602b..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/WordPressXMLRPCAPIFacade.m +++ /dev/null @@ -1,96 +0,0 @@ -#import "WordPressXMLRPCAPIFacade.h" -#import - -@import WordPressKit; - -@interface WordPressXMLRPCAPIFacade () - -@property (nonatomic, strong) NSString *userAgent; - -@end - -NSString *const XMLRPCOriginalErrorKey = @"XMLRPCOriginalErrorKey"; - -@implementation WordPressXMLRPCAPIFacade - -- (instancetype)initWithUserAgent:(NSString *)userAgent -{ - self = [super init]; - if (self) { - _userAgent = userAgent; - } - - return self; -} - -- (void)guessXMLRPCURLForSite:(NSString *)url - success:(void (^)(NSURL *xmlrpcURL))success - failure:(void (^)(NSError *error))failure -{ - WordPressOrgXMLRPCValidator *validator = [[WordPressOrgXMLRPCValidator alloc] init]; - [validator guessXMLRPCURLForSite:url - userAgent:self.userAgent - success:success - failure:^(NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - failure([self errorForGuessXMLRPCApiFailure:error]); - }); - }]; -} - -- (NSError *)errorForGuessXMLRPCApiFailure:(NSError *)error -{ - WPLogError(@"Error on trying to guess XMLRPC site: %@", error); - NSArray *errorCodes = @[ - @(NSURLErrorUserCancelledAuthentication), - @(NSURLErrorNotConnectedToInternet), - @(NSURLErrorNetworkConnectionLost), - ]; - if ([error.domain isEqual:NSURLErrorDomain] && [errorCodes containsObject:@(error.code)]) { - return error; - } else { - NSDictionary *userInfo = @{ - NSLocalizedDescriptionKey: NSLocalizedString(@"Unable to read the WordPress site at that URL. Tap 'Need more help?' to view the FAQ.", nil), - NSLocalizedFailureReasonErrorKey: error.localizedDescription, - XMLRPCOriginalErrorKey: error - }; - - NSError *err = [NSError errorWithDomain:WordPressAuthenticator.errorDomain code:NSURLErrorBadURL userInfo:userInfo]; - return err; - } -} - -- (void)getBlogOptionsWithEndpoint:(NSURL *)xmlrpc - username:(NSString *)username - password:(NSString *)password - success:(void (^)(NSDictionary *options))success - failure:(void (^)(NSError *error))failure; -{ - - WordPressOrgXMLRPCApi *api = [[WordPressOrgXMLRPCApi alloc] initWithEndpoint:xmlrpc userAgent:self.userAgent]; - [api checkCredentials:username password:password success:^(id responseObject, NSHTTPURLResponse *httpResponse __unused) { - dispatch_async(dispatch_get_main_queue(), ^{ - if (![responseObject isKindOfClass:[NSDictionary class]]) { - if (failure) { - NSDictionary *userInfo = @{NSLocalizedDescriptionKey: NSLocalizedString(@"Unable to read the WordPress site at that URL. Tap 'Need more help?' to view the FAQ.", nil)}; - NSError *error = [NSError errorWithDomain:WordPressOrgXMLRPCApiErrorDomain code:WordPressOrgXMLRPCApiErrorResponseSerializationFailed userInfo:userInfo]; - failure(error); - } - return; - } - if (success) { - success((NSDictionary *)responseObject); - } - }); - - } failure:^(NSError *error, NSHTTPURLResponse *httpResponse __unused) { - dispatch_async(dispatch_get_main_queue(), ^{ - if (failure) { - failure(error); - } - }); - }]; -} - - -@end diff --git a/Sources/WordPressAuthenticator/Resources/Animations/jetpack.json b/Sources/WordPressAuthenticator/Resources/Animations/jetpack.json deleted file mode 100644 index 4509ea16b8d9..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Animations/jetpack.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.3.4","fr":30,"ip":0,"op":95,"w":310,"h":464,"nm":"Jetpack","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Jetpack Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.344],"y":[1]},"o":{"x":[0.655],"y":[0]},"n":["0p344_1_0p655_0"],"t":20,"s":[0],"e":[1800]},{"t":50}],"ix":10},"p":{"a":0,"k":[155,291,0],"ix":2},"a":{"a":0,"k":[68,68,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-14.126,-27.274],[-14.126,37.664],[19.293,-27.274]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[85.523,84.065],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[14.124,27.282],[14.124,-37.658],[-19.296,27.282]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[50.476,51.935],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[37.556,0],[0,-37.556],[-37.556,0],[0,37.556]],"o":[[-37.556,0],[0,37.556],[37.556,0],[0,-37.556]],"v":[[0,-68],[-68,0],[0,68],[68,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.8,0.807843137255,0.81568627451,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[68,68],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"iPhone Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155,305,0],"ix":2},"a":{"a":0,"k":[153,303,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,27],[135,27],[135,-27],[-135,-27]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.003921568627,0.376470588235,0.529411764706,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,84],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,217.5],[135,217.5],[135,-217.5],[-135,-217.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,328.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-23.1],[0,0],[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0]],"o":[[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0],[0,0],[0,-23.1],[0,0]],"v":[[-111,-303],[-153,-261],[-153,261],[-111,303],[111,303],[153,261],[153,-261],[111,-303]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.137254901961,0.207843137255,0.294117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,303],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0}],"markers":[]} diff --git a/Sources/WordPressAuthenticator/Resources/Animations/notifications.json b/Sources/WordPressAuthenticator/Resources/Animations/notifications.json deleted file mode 100644 index bafe03becc1d..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Animations/notifications.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.3.4","fr":30,"ip":0,"op":47,"w":310,"h":464,"nm":"Notifications","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"notification-01 Outlines","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":7,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":14,"s":[100],"e":[100]},{"t":17}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155,222.086,0],"ix":2},"a":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[115,73,0],"e":[115,83,0],"to":[0,1.66666662693024,0],"ti":[0,-1.66666662693024,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[115,83,0],"e":[115,83,0],"to":[0,0,0],"ti":[0,0,0]},{"t":17}],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":7,"s":[75,75,100],"e":[103,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":14,"s":[103,103,100],"e":[100,100,100]},{"t":17}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.81,0.213],[3.849,-0.037],[3.849,-0.838],[-0.276,-1.284],[-0.879,-0.193],[-3.849,-0.064],[-3.849,1.004],[0.332,1.285]],"o":[[-3.849,-1.005],[-3.849,0.064],[-1.278,0.278],[0.203,0.942],[3.849,0.836],[3.849,0.038],[1.277,-0.334],[-0.225,-0.872]],"v":[[-54.525,-12.412],[-66.074,-13.557],[-77.622,-12.412],[-79.436,-9.584],[-77.622,-7.761],[-66.074,-6.617],[-54.525,-7.761],[-52.812,-10.691]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[1.162,0.067],[9.639,0.219],[9.64,-0.071],[9.64,-0.253],[9.639,-0.434],[-0.056,-1.284],[-1.181,-0.057],[-9.639,-0.168],[-9.639,-0.106],[-9.64,0.075],[-9.639,0.528],[0.07,1.285]],"o":[[-9.639,-0.53],[-9.64,-0.075],[-9.639,0.107],[-9.639,0.17],[-1.278,0.056],[0.055,1.211],[9.639,0.434],[9.64,0.256],[9.64,0.071],[9.639,-0.219],[1.279,-0.07],[-0.064,-1.194]],"v":[[77.454,-12.377],[48.536,-13.335],[19.616,-13.505],[-9.303,-13.218],[-38.223,-12.377],[-40.434,-9.949],[-38.223,-7.727],[-9.303,-6.889],[19.616,-6.6],[48.536,-6.77],[77.454,-7.727],[79.643,-10.18]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0.81,0.212],[3.85,-0.037],[3.851,-0.838],[-0.276,-1.284],[-0.879,-0.193],[-3.849,-0.063],[-3.85,1.005],[0.333,1.284]],"o":[[-3.85,-1.005],[-3.849,0.063],[-1.277,0.278],[0.203,0.941],[3.851,0.837],[3.85,0.038],[1.278,-0.333],[-0.224,-0.872]],"v":[[53.143,7.742],[41.594,6.597],[30.045,7.742],[28.232,10.569],[30.045,12.392],[41.594,13.536],[53.143,12.392],[54.855,9.463]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[-0.097,-1.15],[1.277,-0.109],[6.212,-0.219],[6.212,0.074],[6.212,0.254],[6.212,0.434],[0.081,1.174],[-1.278,0.088],[-6.211,0.169],[-6.212,0.107],[-6.211,-0.151],[-6.212,-0.529]],"o":[[0.108,1.284],[-6.212,0.529],[-6.211,0.15],[-6.212,-0.107],[-6.211,-0.17],[-1.137,-0.078],[-0.089,-1.286],[6.212,-0.434],[6.212,-0.254],[6.212,-0.075],[6.212,0.219],[1.108,0.093]],"v":[[11.233,9.87],[9.116,12.392],[-9.521,13.349],[-28.156,13.52],[-46.791,13.232],[-65.426,12.392],[-67.579,10.229],[-65.426,7.742],[-46.791,6.902],[-28.156,6.614],[-9.521,6.785],[9.116,7.742]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.882352941176,0.886274509804,0.886274509804,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[113.584,60.612],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,5.545],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[5.525,0]],"v":[[39.855,3.918],[39.855,-13.956],[-39.855,-13.956],[-39.855,13.956],[29.851,13.956]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.003921568627,0.376470588235,0.529411764706,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[113.857,108.956],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-95.5,54.501],[95.5,54.501],[95.5,-54.501],[-95.5,-54.501]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[114.5,80.501],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"iPhone Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155,305,0],"ix":2},"a":{"a":0,"k":[153,303,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,27],[135,27],[135,-27],[-135,-27]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.003921568627,0.376470588235,0.529411764706,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,84],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,217.5],[135,217.5],[135,-217.5],[-135,-217.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.964705882353,0.964705882353,0.964705882353,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,328.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-23.1],[0,0],[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0]],"o":[[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0],[0,0],[0,-23.1],[0,0]],"v":[[-111,-303],[-153,-261],[-153,261],[-111,303],[111,303],[153,261],[153,-261],[111,-303]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.137254901961,0.207843137255,0.294117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,303],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0}],"markers":[]} diff --git a/Sources/WordPressAuthenticator/Resources/Animations/post.json b/Sources/WordPressAuthenticator/Resources/Animations/post.json deleted file mode 100644 index 974f82d2818b..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Animations/post.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.3.4","fr":30,"ip":0,"op":135,"w":310,"h":464,"nm":"post-on-the-go","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Layer 4 Outlines","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":34,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":36,"s":[100],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":82,"s":[100],"e":[0]},{"t":84}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155.028,232.392,0],"ix":2},"a":{"a":0,"k":[115,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.047,0.228],[-4.59,0.075],[-4.589,-1.191],[0.396,-1.524],[0.966,-0.253],[4.589,0.044],[4.59,0.992],[-0.329,1.524]],"o":[[4.59,-0.993],[4.589,-0.045],[1.524,0.395],[-0.267,1.034],[-4.589,1.191],[-4.59,-0.075],[-1.522,-0.329],[0.241,-1.116]],"v":[[-13.742,-2.757],[0.027,-4.115],[13.794,-2.757],[15.836,0.717],[13.794,2.758],[0.027,4.116],[-13.742,2.758],[-15.903,-0.596]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847058823529,0.870588235294,0.894117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[169.994,185.684],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":22,"op":84,"st":22,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Layer 5 Outlines","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":28,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":30,"s":[100],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":84,"s":[100],"e":[0]},{"t":86}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155.028,232.392,0],"ix":2},"a":{"a":0,"k":[115,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.047,0.228],[-4.589,0.075],[-4.589,-1.191],[0.395,-1.524],[0.965,-0.253],[4.59,0.044],[4.589,0.992],[-0.329,1.524]],"o":[[4.589,-0.993],[4.59,-0.045],[1.523,0.395],[-0.268,1.034],[-4.589,1.191],[-4.589,-0.075],[-1.523,-0.329],[0.242,-1.116]],"v":[[-13.741,-2.757],[0.027,-4.115],[13.795,-2.757],[15.837,0.717],[13.795,2.758],[0.027,4.116],[-13.741,2.758],[-15.903,-0.596]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847058823529,0.870588235294,0.894117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[126.146,185.684],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":20,"op":86,"st":20,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Layer 6 Outlines","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":25,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":27,"s":[100],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":87,"s":[100],"e":[0]},{"t":89}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155.028,232.392,0],"ix":2},"a":{"a":0,"k":[115,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.357,0.093],[-7.415,0.2],[-7.415,0.127],[-7.415,-0.179],[-7.415,-0.628],[0.13,-1.524],[1.323,-0.111],[7.415,-0.26],[7.416,0.088],[7.415,0.301],[7.415,0.514],[-0.106,1.525]],"o":[[7.415,-0.515],[7.415,-0.303],[7.416,-0.089],[7.415,0.259],[1.525,0.129],[-0.115,1.367],[-7.415,0.628],[-7.415,0.179],[-7.415,-0.127],[-7.415,-0.202],[-1.525,-0.106],[0.097,-1.396]],"v":[[-44.611,-2.761],[-22.365,-3.757],[-0.12,-4.1],[22.126,-3.897],[44.372,-2.761],[46.899,0.234],[44.372,2.762],[22.126,3.898],[-0.12,4.101],[-22.365,3.759],[-44.611,2.762],[-47.181,-0.191]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847058823529,0.870588235294,0.894117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[47.029,185.684],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":17,"op":89,"st":17,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Layer 3 Outlines","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":16,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":18,"s":[100],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":93,"s":[100],"e":[0]},{"t":95}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155.028,232.392,0],"ix":2},"a":{"a":0,"k":[115,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.302,0.067],[-10.625,0.201],[-10.625,0.127],[-10.625,-0.089],[-10.625,-0.629],[0.077,-1.525],[1.279,-0.081],[10.624,-0.259],[10.624,0.084],[10.624,0.302],[10.624,0.515],[-0.063,1.525]],"o":[[10.624,-0.515],[10.624,-0.302],[10.624,-0.084],[10.624,0.26],[1.408,0.083],[-0.071,1.419],[-10.625,0.628],[-10.625,0.09],[-10.625,-0.127],[-10.625,-0.201],[-1.409,-0.068],[0.06,-1.439]],"v":[[-63.704,-2.761],[-31.83,-3.758],[0.043,-4.1],[31.917,-3.898],[63.79,-2.761],[66.2,0.151],[63.79,2.761],[31.917,3.897],[0.043,4.1],[-31.83,3.758],[-63.704,2.761],[-66.14,-0.123]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847058823529,0.870588235294,0.894117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[163.797,162.199],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":11,"op":95,"st":11,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Layer 7 Outlines","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":9,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":11,"s":[100],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":95,"s":[100],"e":[0]},{"t":97}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155.028,232.392,0],"ix":2},"a":{"a":0,"k":[115,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.33,0.107],[-6.293,0.2],[-6.293,0.127],[-6.294,-0.179],[-6.293,-0.628],[0.153,-1.525],[1.288,-0.126],[6.292,-0.26],[6.293,0.089],[6.293,0.301],[6.292,0.515],[-0.125,1.525]],"o":[[6.292,-0.515],[6.293,-0.303],[6.293,-0.089],[6.292,0.259],[1.525,0.152],[-0.133,1.338],[-6.293,0.629],[-6.294,0.179],[-6.293,-0.127],[-6.293,-0.202],[-1.525,-0.124],[0.112,-1.373]],"v":[[-37.884,-2.761],[-19.005,-3.757],[-0.126,-4.1],[18.754,-3.897],[37.633,-2.761],[40.118,0.276],[37.633,2.761],[18.754,3.898],[-0.126,4.1],[-19.005,3.759],[-37.884,2.761],[-40.42,-0.225]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847058823529,0.870588235294,0.894117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[40.271,162.189],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":9,"op":97,"st":9,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"text-bodymovin Outlines 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155,232.087,0],"ix":2},"a":{"a":0,"k":[115,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.242,-1.118],[-1.525,-0.33],[-4.596,-0.076],[-4.594,1.193],[-0.268,1.037],[1.526,0.396],[4.595,-0.044],[4.595,-0.993]],"o":[[-0.33,1.524],[4.595,0.995],[4.595,0.044],[0.967,-0.254],[0.396,-1.525],[-4.594,-1.192],[-4.596,0.076],[-1.049,0.228]],"v":[[-114.981,28.062],[-112.816,31.42],[-99.03,32.78],[-85.245,31.42],[-83.2,29.375],[-85.245,25.897],[-99.03,24.538],[-112.816,25.897]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.06,-1.438],[-1.408,-0.069],[-10.625,-0.201],[-10.625,-0.126],[-10.625,0.09],[-10.625,0.628],[-0.071,1.419],[1.408,0.084],[10.624,0.259],[10.624,-0.085],[10.624,-0.302],[10.624,-0.514]],"o":[[-0.063,1.526],[10.624,0.514],[10.624,0.302],[10.624,0.085],[10.624,-0.259],[1.279,-0.081],[0.077,-1.526],[-10.625,-0.628],[-10.625,-0.09],[-10.625,0.127],[-10.625,0.201],[-1.302,0.068]],"v":[[-17.344,-28.749],[-14.908,-25.863],[16.966,-24.867],[48.839,-24.525],[80.713,-24.728],[112.586,-25.863],[114.996,-28.474],[112.586,-31.387],[80.713,-32.522],[48.839,-32.725],[16.966,-32.383],[-14.908,-31.387]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0.112,-1.372],[-1.525,-0.125],[-6.293,-0.201],[-6.293,-0.127],[-6.294,0.179],[-6.293,0.628],[-0.134,1.338],[1.525,0.152],[6.292,0.26],[6.293,-0.088],[6.294,-0.302],[6.293,-0.515]],"o":[[-0.125,1.525],[6.293,0.515],[6.294,0.302],[6.293,0.088],[6.292,-0.26],[1.288,-0.128],[0.152,-1.525],[-6.293,-0.628],[-6.294,-0.179],[-6.293,0.127],[-6.293,0.201],[-1.329,0.107]],"v":[[-115.149,-28.861],[-112.613,-25.874],[-93.734,-24.878],[-74.855,-24.535],[-55.975,-24.738],[-37.096,-25.874],[-34.61,-28.36],[-37.096,-31.397],[-55.975,-32.533],[-74.855,-32.736],[-93.734,-32.393],[-112.613,-31.397]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[0.065,-1.438],[-1.526,-0.067],[-11.508,-0.201],[-11.508,-0.127],[-11.508,0.089],[-11.508,0.629],[-0.077,1.419],[1.525,0.083],[11.507,0.259],[11.507,-0.084],[11.507,-0.303],[11.507,-0.515]],"o":[[-0.068,1.525],[11.507,0.515],[11.507,0.302],[11.507,0.084],[11.507,-0.26],[1.385,-0.08],[0.084,-1.525],[-11.508,-0.628],[-11.508,-0.09],[-11.508,0.127],[-11.508,0.2],[-1.41,0.067]],"v":[[-68.421,28.495],[-65.783,31.379],[-31.26,32.376],[3.262,32.718],[37.785,32.516],[72.308,31.379],[74.918,28.769],[72.308,25.857],[37.785,24.721],[3.262,24.518],[-31.26,24.861],[-65.783,25.857]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[0.242,-1.117],[-1.525,-0.33],[-4.596,-0.075],[-4.596,1.192],[-0.268,1.036],[1.525,0.396],[4.596,-0.044],[4.595,-0.994]],"o":[[-0.329,1.526],[4.595,0.994],[4.596,0.044],[0.967,-0.253],[0.396,-1.525],[-4.596,-1.193],[-4.596,0.075],[-1.049,0.229]],"v":[[-0.778,-0.621],[1.387,2.738],[15.173,4.097],[28.959,2.738],[31.003,0.693],[28.959,-2.785],[15.173,-4.144],[1.387,-2.785]],"c":true},"ix":2},"nm":"Path 5","mn":"ADBE Vector Shape - Group","hd":false},{"ind":5,"ty":"sh","ix":6,"ks":{"a":0,"k":{"i":[[-1.357,0.093],[-7.415,0.201],[-7.415,0.127],[-7.415,-0.179],[-7.415,-0.629],[0.129,-1.525],[1.323,-0.111],[7.415,-0.259],[7.415,0.089],[7.416,0.303],[7.415,0.514],[-0.106,1.525]],"o":[[7.415,-0.514],[7.416,-0.302],[7.415,-0.088],[7.415,0.259],[1.526,0.129],[-0.116,1.367],[-7.415,0.628],[-7.415,0.179],[-7.415,-0.127],[-7.415,-0.2],[-1.525,-0.106],[0.096,-1.396]],"v":[[-112.582,-2.785],[-90.336,-3.782],[-68.09,-4.124],[-45.844,-3.921],[-23.599,-2.785],[-21.071,0.21],[-23.599,2.738],[-45.844,3.874],[-68.09,4.077],[-90.336,3.734],[-112.582,2.738],[-115.151,-0.215]],"c":true},"ix":2},"nm":"Path 6","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847058823529,0.870588235294,0.894117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[115,104.848],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":7,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"text-bodymovin Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155,232.087,0],"ix":2},"a":{"a":0,"k":[115,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[12.516,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,-12.516],[0,0],[0,0],[0,0]],"v":[[-96.144,18.343],[115.035,18.343],[115.035,4.32],[92.373,-18.343],[-115,-18.343],[-115,18.343]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.435294117647,0.576470588235,0.678431372549,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[115,18.343],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Image Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[155,374,0],"e":[155,402,0],"to":[0,4.66666650772095,0],"ti":[0,-4.66666650772095,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[155,402,0],"e":[155,402,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":25,"s":[155,402,0],"e":[155,430,0],"to":[0,4.66666650772095,0],"ti":[0,-4.66666650772095,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":27,"s":[155,430,0],"e":[155,430,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":87,"s":[155,430,0],"e":[155,402,0],"to":[0,-4.66666650772095,0],"ti":[0,4.66666650772095,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":89,"s":[155,402,0],"e":[155,402,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":94,"s":[155,402,0],"e":[155,374,0],"to":[0,-4.66666650772095,0],"ti":[0,4.66666650772095,0]},{"t":96}],"ix":2},"a":{"a":0,"k":[115,70,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-12.15],[12.15,0],[0,12.15],[-12.15,0]],"o":[[0,12.15],[-12.15,0],[0,-12.15],[12.15,0]],"v":[[22,0],[0,22],[-22,0],[0,-22]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[191.039,37.805],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,1.83],[0,0],[26.664,3.881],[44.378,51.841],[0,0],[0,0]],"o":[[0,0],[-11.598,-14.112],[-48.045,-6.995],[0,0],[0,0],[1.83,0]],"v":[[115,46.772],[115,19.79],[56.449,-6.075],[-115,-50.085],[-115,50.085],[111.687,50.085]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.435294117647,0.576470588235,0.678431372549,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[115,89.771],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.83,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,1.83]],"v":[[111.687,69.855],[-115,69.855],[-115,-69.855],[115,-69.855],[115,66.542]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.733333333333,0.788235294118,0.835294117647,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[115,70.001],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":4,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"iPhone Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155,305,0],"ix":2},"a":{"a":0,"k":[153,303,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,27],[135,27],[135,-27],[-135,-27]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.003921568627,0.376470588235,0.529411764706,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,84],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,217.5],[135,217.5],[135,-217.5],[-135,-217.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,328.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-23.1],[0,0],[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0]],"o":[[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0],[0,0],[0,-23.1],[0,0]],"v":[[-111,-303],[-153,-261],[-153,261],[-111,303],[111,303],[153,261],[153,-261],[111,-303]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.137254901961,0.207843137255,0.294117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153.328,303.21],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0}],"markers":[]} diff --git a/Sources/WordPressAuthenticator/Resources/Animations/reader.json b/Sources/WordPressAuthenticator/Resources/Animations/reader.json deleted file mode 100644 index 0d9cd7a9b3b7..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Animations/reader.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"4.7.0","fr":30,"ip":0,"op":150,"w":306,"h":462,"nm":"Reader","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Mask Outlines","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[152.724,28.147,0]},"a":{"a":0,"k":[135,217.5,0]},"s":{"a":0,"k":[84.074,13.103,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0]],"o":[[0,0]],"v":[[185.879,-169.017]],"c":false}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":2},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[0.1372549,0.2078431,0.2941176,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-134.937,27.126],[135.188,27.06],[135.25,-27.016],[-135,-27]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.0039216,0.3764706,0.5294118,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[135.174,643.654],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[118.777,759.676],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-141.244,223.056],[146.374,223.056],[136.711,-211.481],[-136.783,-210.554],[-140.45,-160.256]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.1372549,0.2078431,0.2941177,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[135,217.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100.332,103.011],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group"}],"ip":0,"op":419,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":4,"nm":"Cards Outlines","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.967],"y":[1]},"o":{"x":[0.158],"y":[0]},"n":["0p967_1_0p158_0"],"t":10,"s":[153],"e":[153]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.205],"y":[0]},"n":["0p833_1_0p205_0"],"t":46,"s":[153],"e":[153]},{"i":{"x":[0.985],"y":[1]},"o":{"x":[0.015],"y":[0]},"n":["0p985_1_0p015_0"],"t":90,"s":[153],"e":[153]},{"t":110}]},"y":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.167],"y":[0]},"n":["1_1_0p167_0"],"t":0,"s":[431],"e":[432]},{"i":{"x":[0.069],"y":[1]},"o":{"x":[0.216],"y":[0]},"n":["0p069_1_0p216_0"],"t":10,"s":[432],"e":[98]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.205],"y":[0.185]},"n":["0p833_1_0p205_0p185"],"t":44,"s":[98],"e":[98]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.301],"y":[0]},"n":["0_1_0p301_0"],"t":90,"s":[98],"e":[432]},{"t":118}]}},"a":{"a":0,"k":[115,300,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.047,-0.229],[-4.59,-0.075],[-4.589,1.191],[0.396,1.524],[0.966,0.253],[4.589,-0.044],[4.59,-0.992],[-0.329,-1.524]],"o":[[4.59,0.992],[4.589,0.044],[1.524,-0.396],[-0.267,-1.035],[-4.589,-1.191],[-4.59,0.075],[-1.522,0.329],[0.241,1.115]],"v":[[41.252,-80.641],[55.021,-79.284],[68.788,-80.641],[70.83,-84.115],[68.788,-86.157],[55.021,-87.514],[41.252,-86.157],[39.091,-82.802]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-1.048,-0.229],[-4.589,-0.075],[-4.588,1.191],[0.396,1.524],[0.966,0.253],[4.59,-0.044],[4.589,-0.992],[-0.329,-1.524]],"o":[[4.589,0.992],[4.59,0.044],[1.524,-0.396],[-0.267,-1.035],[-4.588,-1.191],[-4.589,0.075],[-1.523,0.329],[0.241,1.115]],"v":[[-2.595,-80.641],[11.173,-79.284],[24.94,-80.641],[26.982,-84.115],[24.94,-86.157],[11.173,-87.514],[-2.595,-86.157],[-4.757,-82.802]],"c":true}},"nm":"Path 2","mn":"ADBE Vector Shape - Group"},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[-1.358,-0.093],[-7.415,-0.201],[-7.415,-0.127],[-7.415,0.178],[-7.415,0.628],[0.129,1.525],[1.323,0.112],[7.415,0.259],[7.415,-0.088],[7.416,-0.301],[7.415,-0.514],[-0.106,-1.526]],"o":[[7.415,0.515],[7.416,0.302],[7.415,0.088],[7.415,-0.26],[1.526,-0.129],[-0.116,-1.366],[-7.415,-0.627],[-7.415,-0.179],[-7.415,0.127],[-7.415,0.202],[-1.526,0.107],[0.096,1.395]],"v":[[-112.581,-80.637],[-90.336,-79.641],[-68.09,-79.298],[-45.844,-79.501],[-23.599,-80.637],[-21.071,-83.633],[-23.599,-86.161],[-45.844,-87.296],[-68.09,-87.499],[-90.336,-87.157],[-112.581,-86.161],[-115.151,-83.207]],"c":true}},"nm":"Path 3","mn":"ADBE Vector Shape - Group"},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[0.241,-1.116],[-1.522,-0.329],[-4.59,-0.076],[-4.589,1.191],[-0.267,1.035],[1.524,0.396],[4.589,-0.044],[4.59,-0.992]],"o":[[-0.329,1.523],[4.59,0.993],[4.589,0.044],[0.966,-0.253],[0.396,-1.523],[-4.589,-1.191],[-4.59,0.075],[-1.047,0.229]],"v":[[39.091,177.251],[41.252,180.605],[55.021,181.963],[68.788,180.605],[70.83,178.563],[68.788,175.089],[55.021,173.732],[41.252,175.089]],"c":true}},"nm":"Path 4","mn":"ADBE Vector Shape - Group"},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[0.241,-1.116],[-1.523,-0.329],[-4.589,-0.076],[-4.588,1.191],[-0.267,1.035],[1.524,0.396],[4.59,-0.044],[4.589,-0.992]],"o":[[-0.329,1.523],[4.589,0.993],[4.59,0.044],[0.966,-0.253],[0.396,-1.523],[-4.588,-1.191],[-4.589,0.075],[-1.048,0.229]],"v":[[-4.757,177.251],[-2.595,180.605],[11.173,181.963],[24.94,180.605],[26.982,178.563],[24.94,175.089],[11.173,173.732],[-2.595,175.089]],"c":true}},"nm":"Path 5","mn":"ADBE Vector Shape - Group"},{"ind":5,"ty":"sh","ix":6,"ks":{"a":0,"k":{"i":[[0.096,-1.396],[-1.526,-0.105],[-7.415,-0.2],[-7.415,-0.127],[-7.415,0.178],[-7.415,0.628],[-0.116,1.366],[1.526,0.129],[7.415,0.26],[7.415,-0.088],[7.416,-0.301],[7.415,-0.515]],"o":[[-0.106,1.525],[7.415,0.515],[7.416,0.303],[7.415,0.089],[7.415,-0.259],[1.323,-0.111],[0.129,-1.526],[-7.415,-0.628],[-7.415,-0.179],[-7.415,0.127],[-7.415,0.202],[-1.358,0.092]],"v":[[-115.151,177.656],[-112.581,180.609],[-90.336,181.605],[-68.09,181.948],[-45.844,181.745],[-23.599,180.609],[-21.071,178.082],[-23.599,175.086],[-45.844,173.95],[-68.09,173.747],[-90.336,174.089],[-112.581,175.086]],"c":true}},"nm":"Path 6","mn":"ADBE Vector Shape - Group"},{"ind":6,"ty":"sh","ix":7,"ks":{"a":0,"k":{"i":[[0.06,-1.438],[-1.408,-0.069],[-10.625,-0.201],[-10.625,-0.126],[-10.625,0.09],[-10.625,0.628],[-0.071,1.419],[1.408,0.083],[10.624,0.259],[10.624,-0.084],[10.624,-0.303],[10.624,-0.514]],"o":[[-0.063,1.525],[10.624,0.513],[10.624,0.301],[10.624,0.084],[10.624,-0.259],[1.279,-0.081],[0.077,-1.525],[-10.625,-0.628],[-10.625,-0.089],[-10.625,0.127],[-10.625,0.2],[-1.302,0.068]],"v":[[-17.344,154.239],[-14.908,157.125],[16.966,158.121],[48.839,158.463],[80.713,158.26],[112.586,157.125],[114.996,154.513],[112.586,151.601],[80.713,150.465],[48.839,150.262],[16.966,150.605],[-14.908,151.601]],"c":true}},"nm":"Path 7","mn":"ADBE Vector Shape - Group"},{"ind":7,"ty":"sh","ix":8,"ks":{"a":0,"k":{"i":[[0.112,-1.372],[-1.525,-0.125],[-6.293,-0.201],[-6.292,-0.127],[-6.294,0.178],[-6.293,0.628],[-0.134,1.337],[1.525,0.153],[6.292,0.259],[6.294,-0.088],[6.294,-0.301],[6.293,-0.514]],"o":[[-0.125,1.525],[6.293,0.515],[6.294,0.303],[6.294,0.088],[6.292,-0.26],[1.288,-0.128],[0.153,-1.526],[-6.293,-0.627],[-6.294,-0.179],[-6.292,0.127],[-6.293,0.202],[-1.329,0.108]],"v":[[-115.149,154.127],[-112.613,157.114],[-93.734,158.11],[-74.855,158.453],[-55.975,158.25],[-37.096,157.114],[-34.61,154.628],[-37.096,151.59],[-55.975,150.455],[-74.855,150.252],[-93.734,150.594],[-112.613,151.59]],"c":true}},"nm":"Path 8","mn":"ADBE Vector Shape - Group"},{"ind":8,"ty":"sh","ix":9,"ks":{"a":0,"k":{"i":[[0.242,-1.117],[-1.525,-0.329],[-4.595,-0.075],[-4.594,1.193],[-0.268,1.036],[1.526,0.396],[4.595,-0.045],[4.595,-0.994]],"o":[[-0.33,1.526],[4.595,0.994],[4.595,0.045],[0.967,-0.253],[0.396,-1.525],[-4.594,-1.193],[-4.595,0.075],[-1.049,0.229]],"v":[[-114.981,-121.151],[-112.816,-117.792],[-99.03,-116.433],[-85.245,-117.792],[-83.2,-119.837],[-85.245,-123.315],[-99.03,-124.674],[-112.816,-123.315]],"c":true}},"nm":"Path 9","mn":"ADBE Vector Shape - Group"},{"ind":9,"ty":"sh","ix":10,"ks":{"a":0,"k":{"i":[[0.06,-1.438],[-1.408,-0.068],[-10.625,-0.201],[-10.625,-0.127],[-10.625,0.09],[-10.625,0.628],[-0.071,1.419],[1.408,0.083],[10.624,0.26],[10.624,-0.085],[10.624,-0.302],[10.624,-0.514]],"o":[[-0.063,1.526],[10.624,0.515],[10.624,0.302],[10.624,0.084],[10.624,-0.259],[1.279,-0.081],[0.077,-1.526],[-10.625,-0.628],[-10.625,-0.09],[-10.625,0.127],[-10.625,0.201],[-1.302,0.068]],"v":[[-17.344,-177.961],[-14.908,-175.076],[16.966,-174.079],[48.839,-173.737],[80.713,-173.94],[112.586,-175.076],[114.996,-177.686],[112.586,-180.599],[80.713,-181.735],[48.839,-181.937],[16.966,-181.595],[-14.908,-180.599]],"c":true}},"nm":"Path 10","mn":"ADBE Vector Shape - Group"},{"ind":10,"ty":"sh","ix":11,"ks":{"a":0,"k":{"i":[[0.112,-1.372],[-1.525,-0.125],[-6.293,-0.201],[-6.292,-0.127],[-6.294,0.179],[-6.293,0.628],[-0.134,1.338],[1.525,0.152],[6.292,0.259],[6.294,-0.089],[6.294,-0.303],[6.293,-0.515]],"o":[[-0.125,1.526],[6.293,0.514],[6.294,0.302],[6.294,0.088],[6.292,-0.26],[1.288,-0.128],[0.153,-1.526],[-6.293,-0.628],[-6.294,-0.179],[-6.292,0.127],[-6.293,0.2],[-1.329,0.107]],"v":[[-115.149,-178.074],[-112.613,-175.086],[-93.734,-174.09],[-74.855,-173.747],[-55.975,-173.95],[-37.096,-175.086],[-34.61,-177.572],[-37.096,-180.609],[-55.975,-181.745],[-74.855,-181.948],[-93.734,-181.605],[-112.613,-180.609]],"c":true}},"nm":"Path 11","mn":"ADBE Vector Shape - Group"},{"ind":11,"ty":"sh","ix":12,"ks":{"a":0,"k":{"i":[[0.065,-1.439],[-1.526,-0.069],[-11.508,-0.201],[-11.508,-0.126],[-11.508,0.09],[-11.508,0.628],[-0.077,1.418],[1.525,0.084],[11.507,0.26],[11.508,-0.084],[11.507,-0.302],[11.507,-0.514]],"o":[[-0.068,1.525],[11.507,0.514],[11.507,0.302],[11.508,0.084],[11.507,-0.258],[1.385,-0.081],[0.084,-1.525],[-11.508,-0.628],[-11.508,-0.089],[-11.508,0.127],[-11.508,0.201],[-1.41,0.068]],"v":[[-68.421,-120.717],[-65.783,-117.832],[-31.26,-116.836],[3.262,-116.494],[37.785,-116.697],[72.308,-117.832],[74.918,-120.443],[72.308,-123.356],[37.785,-124.492],[3.262,-124.695],[-31.26,-124.352],[-65.783,-123.356]],"c":true}},"nm":"Path 12","mn":"ADBE Vector Shape - Group"},{"ind":12,"ty":"sh","ix":13,"ks":{"a":0,"k":{"i":[[0.242,-1.118],[-1.525,-0.33],[-4.596,-0.075],[-4.596,1.192],[-0.268,1.036],[1.525,0.395],[4.596,-0.044],[4.595,-0.995]],"o":[[-0.329,1.525],[4.595,0.993],[4.596,0.045],[0.967,-0.254],[0.396,-1.525],[-4.596,-1.193],[-4.596,0.076],[-1.049,0.228]],"v":[[-0.778,-149.833],[1.387,-146.474],[15.173,-145.116],[28.959,-146.474],[31.003,-148.519],[28.959,-151.997],[15.173,-153.357],[1.387,-151.997]],"c":true}},"nm":"Path 13","mn":"ADBE Vector Shape - Group"},{"ind":13,"ty":"sh","ix":14,"ks":{"a":0,"k":{"i":[[-1.358,0.092],[-7.415,0.201],[-7.415,0.127],[-7.415,-0.179],[-7.415,-0.628],[0.129,-1.525],[1.323,-0.111],[7.415,-0.26],[7.415,0.088],[7.416,0.302],[7.415,0.515],[-0.106,1.524]],"o":[[7.415,-0.515],[7.416,-0.302],[7.415,-0.089],[7.415,0.259],[1.526,0.128],[-0.116,1.367],[-7.415,0.628],[-7.415,0.178],[-7.415,-0.127],[-7.415,-0.201],[-1.526,-0.106],[0.096,-1.396]],"v":[[-112.581,-151.997],[-90.336,-152.994],[-68.09,-153.336],[-45.844,-153.133],[-23.599,-151.997],[-21.071,-149.002],[-23.599,-146.474],[-45.844,-145.338],[-68.09,-145.135],[-90.336,-145.478],[-112.581,-146.474],[-115.151,-149.427]],"c":true}},"nm":"Path 14","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.8470588,0.8705882,0.8941176,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[115,354.135],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":15,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.464,-1.615],[-2.528,5.622]],"o":[[-3.106,3.428],[1.811,-4.028]],"v":[[-1.597,-3.845],[2.892,-0.161]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.9529412,0.9607843,0.9647059,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[126.659,443.744],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.818,2.02],[3.963,-4.719]],"o":[[1.736,-4.288],[-2.84,3.382]],"v":[[-4.207,-0.767],[1.062,1.673]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.9529412,0.9607843,0.9647059,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[201.302,439.586],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.817,2.02],[2.556,-3.602]],"o":[[1.737,-4.288],[-2.555,3.602]],"v":[[-2.993,-1.016],[1.254,1.702]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.9529412,0.9607843,0.9647059,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[177.04,420.634],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"ix":4,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.178,-0.091],[-4.251,1.194]],"o":[[-4.622,0.192],[4.252,-1.194]],"v":[[-0.38,-3.009],[0.751,1.905]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.9529412,0.9607843,0.9647059,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[198.404,379.927],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":2,"cix":2,"ix":5,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.456,-1.241],[-4.251,1.194]],"o":[[-4.456,1.241],[4.252,-1.194]],"v":[[-0.463,-2.434],[0.668,2.48]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.9529412,0.9607843,0.9647059,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[154.751,386.841],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"ix":6,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.397,-1.477],[-0.458,-0.982],[-0.423,-0.219],[-0.108,-0.053],[-0.159,-0.008],[-0.338,0.02],[-1.422,0.465],[-2.715,1.657],[-2.597,1.948],[-4.737,4.567],[-4.492,4.831],[-4.051,5.145],[-2.962,5.657],[-0.774,2.887],[-0.142,0.714],[-0.036,0.346],[0,0],[0,0],[0.596,0.653],[1.319,0.095],[0.711,-0.06],[0.74,-0.225],[2.825,-1.639],[0,0],[0.339,0.585],[-0.445,0.375],[-3.349,1.406],[-2.038,0.076],[-0.261,-0.012],[0,0],[-0.268,-0.043],[0,0],[-0.267,-0.12],[0,0],[-0.462,-0.401],[-0.379,-0.473],[-0.265,-0.522],[-0.075,-1.012],[0.365,-1.807],[1.474,-3.168],[4.036,-5.49],[4.542,-5.015],[4.979,-4.615],[5.628,-3.936],[3.109,-1.6],[1.665,-0.631],[0.844,-0.289],[0.913,-0.167],[2.069,0.423],[0.509,0.181],[0.25,0.137],[0.223,0.216],[0.332,1.105],[-0.788,1.626],[-1.188,1.25],[-0.246,-0.233],[0.132,-0.239]],"o":[[-0.795,1.435],[-0.403,1.458],[0.264,0.452],[0.081,0.076],[0.129,0.035],[0.297,0.03],[1.236,0.071],[2.852,-0.899],[2.757,-1.605],[5.206,-3.894],[4.799,-4.498],[4.516,-4.81],[4.032,-5.149],[1.473,-2.818],[0.229,-0.729],[0.063,-0.355],[0,0],[0,0],[-0.028,-1.302],[-0.571,-0.67],[-0.644,-0.077],[-0.722,0.14],[-2.974,0.781],[0,0],[-0.584,0.338],[-0.302,-0.52],[2.62,-2.218],[1.671,-0.705],[0.255,-0.007],[0,0],[0.267,0.03],[0,0],[0.272,0.088],[0,0],[0.544,0.251],[0.496,0.378],[0.367,0.485],[0.404,1.042],[0.202,2.043],[-0.705,3.637],[-3.008,6.314],[-4.055,5.482],[-4.58,4.981],[-5.027,4.557],[-2.822,1.957],[-1.561,0.785],[-0.811,0.344],[-0.874,0.25],[-1.813,0.335],[-0.504,-0.07],[-0.256,-0.078],[-0.247,-0.163],[-0.96,-0.749],[-0.497,-2.26],[0.819,-1.639],[0.233,-0.245],[0.207,0.197],[0,0]],"v":[[-38.758,32.822],[-40.631,37.209],[-40.629,41.168],[-39.655,42.181],[-39.309,42.323],[-38.941,42.447],[-38.075,42.566],[-33.98,41.953],[-25.566,37.936],[-17.577,32.487],[-2.702,19.679],[11.171,5.569],[24.045,-9.373],[34.8,-25.553],[38.298,-34.177],[38.743,-36.321],[38.834,-37.353],[38.876,-37.867],[38.848,-38.345],[37.918,-41.382],[35.012,-42.575],[32.942,-42.502],[30.75,-42.031],[22.038,-38.12],[22.031,-38.116],[20.359,-38.561],[20.627,-40.108],[29.515,-45.652],[35.053,-46.937],[35.818,-46.957],[36.617,-46.881],[37.418,-46.784],[38.238,-46.552],[39.051,-46.281],[39.845,-45.866],[41.305,-44.798],[42.474,-43.421],[43.288,-41.882],[44.039,-38.76],[43.681,-32.991],[40.156,-22.894],[29.27,-5.393],[16.249,10.255],[1.924,24.636],[-13.991,37.469],[-22.864,42.84],[-27.696,44.975],[-30.238,45.846],[-32.911,46.478],[-38.747,46.546],[-40.302,46.046],[-41.065,45.639],[-41.794,45.148],[-43.744,42.192],[-42.832,36.36],[-39.729,32.085],[-38.862,32.063],[-38.748,32.804]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.0039216,0.3764706,0.5294118,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[76.621,402.73],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":2,"cix":2,"ix":7,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.953,-21.002],[21.895,7.258],[-2.144,21.472],[-21.188,-4.084]],"o":[[-4.953,21.002],[-20.482,-6.789],[2.407,-24.099],[22.139,4.268]],"v":[[36.814,8.758],[-9.791,35.815],[-39.622,-7.907],[6.122,-38.989]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[77.475,404.954],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":2,"cix":2,"ix":8,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[115,69.859],[-115.538,69.859],[-115.538,-69.859],[115,-69.859]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.7333333,0.7882353,0.8352941,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[115,403.141],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9","np":2,"cix":2,"ix":9,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-12.15],[12.15,0],[0,12.15],[-12.15,0]],"o":[[0,12.15],[-12.15,0],[0,-12.15],[12.15,0]],"v":[[22,0],[0,22],[-22,0],[0,-22]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[191.039,37.805],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 10","np":2,"cix":2,"ix":10,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,1.83],[0,0],[26.664,3.882],[44.378,51.842],[0,0],[0,0]],"o":[[0,0],[-11.598,-14.111],[-48.045,-6.994],[0,0],[0,0],[1.83,0]],"v":[[115,46.771],[115,19.788],[56.449,-6.074],[-115,-50.084],[-115,50.084],[111.687,50.084]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.4352941,0.5764706,0.6784314,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[115,89.771],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 11","np":2,"cix":2,"ix":11,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.83,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,1.83]],"v":[[111.687,69.855],[-115,69.855],[-115,-69.855],[115,-69.855],[115,66.542]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.7333333,0.7882353,0.8352941,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[115,70],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 12","np":2,"cix":2,"ix":12,"mn":"ADBE Vector Group"}],"ip":0,"op":419,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"iPhone Outlines","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[153,303,0]},"a":{"a":0,"k":[153,303,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,27],[135,27],[135,-27],[-135,-27]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.0039216,0.3764706,0.5294118,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[153,84],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,217.5],[135,217.5],[135,-217.5],[-135,-217.5]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[153,328.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-23.1],[0,0],[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0]],"o":[[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0],[0,0],[0,-23.1],[0,0]],"v":[[-111,-303],[-153,-261],[-153,261],[-111,303],[111,303],[153,261],[153,-261],[111,-303]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.1372549,0.2078431,0.2941176,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[153.134,303.104],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group"}],"ip":0,"op":419,"st":0,"bm":0,"sr":1}]} diff --git a/Sources/WordPressAuthenticator/Resources/Animations/stats.json b/Sources/WordPressAuthenticator/Resources/Animations/stats.json deleted file mode 100644 index 35557355eebc..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Animations/stats.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.3.4","fr":30,"ip":0,"op":67,"w":310,"h":464,"nm":"Stats","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"stats-01 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155,274.891,0],"ix":2},"a":{"a":0,"k":[115,117,0],"ix":1},"s":{"a":0,"k":[144.195,144.195,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.161,0.067],[9.64,0.219],[9.64,-0.071],[9.64,-0.254],[9.639,-0.434],[-0.057,-1.284],[-1.181,-0.056],[-9.64,-0.169],[-9.64,-0.107],[-9.64,0.076],[-9.64,0.529],[0.07,1.284]],"o":[[-9.64,-0.53],[-9.64,-0.075],[-9.64,0.106],[-9.64,0.17],[-1.279,0.056],[0.055,1.211],[9.639,0.434],[9.64,0.255],[9.64,0.071],[9.64,-0.218],[1.278,-0.07],[-0.065,-1.195]],"v":[[57.921,-14.887],[29.002,-15.845],[0.082,-16.014],[-28.837,-15.727],[-57.756,-14.887],[-59.967,-12.458],[-57.756,-10.237],[-28.837,-9.398],[0.082,-9.109],[29.002,-9.28],[57.921,-10.237],[60.109,-12.689]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-0.071,-1.15],[0.94,-0.109],[4.57,-0.218],[4.571,0.075],[4.571,0.254],[4.571,0.433],[0.06,1.175],[-0.94,0.089],[-4.57,0.169],[-4.571,0.107],[-4.571,-0.152],[-4.57,-0.529]],"o":[[0.08,1.284],[-4.57,0.529],[-4.571,0.15],[-4.571,-0.107],[-4.57,-0.17],[-0.836,-0.079],[-0.065,-1.285],[4.571,-0.433],[4.571,-0.255],[4.571,-0.075],[4.57,0.218],[0.815,0.093]],"v":[[-2.124,12.36],[-3.682,14.883],[-17.394,15.839],[-31.106,16.01],[-44.818,15.722],[-58.53,14.883],[-60.114,12.719],[-58.53,10.232],[-44.818,9.393],[-31.106,9.105],[-17.394,9.276],[-3.682,10.232]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.8,0.807843137255,0.81568627451,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[112.118,59.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-15.628,22.126],[15.628,22.126],[15.628,-22.126],[-15.628,-22.126]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.137254901961,0.207843137255,0.294117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[162.101,209.788],"ix":2},"a":{"a":0,"k":[0,22],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":21,"s":[100,0],"e":[100,100]},{"t":35}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"bar 3-2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-15.628,51.566],[15.628,51.566],[15.628,-51.567],[-15.628,-51.567]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.274509803922,0.474509803922,0.603921568627,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[162.101,209.9],"ix":2},"a":{"a":0,"k":[0,51.5],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":16,"s":[100,0],"e":[100,100]},{"t":30}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"bar 3-1","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-15.628,14.665],[15.628,14.665],[15.628,-14.665],[-15.628,-14.665]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.137254901961,0.207843137255,0.294117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[115.079,209.776],"ix":2},"a":{"a":0,"k":[0,14.5],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":14,"s":[100,0],"e":[100,100]},{"t":29}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"bar 2-2","np":2,"cix":2,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-15.628,34.178],[15.628,34.178],[15.628,-34.178],[-15.628,-34.178]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.274509803922,0.474509803922,0.603921568627,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[115,209.736],"ix":2},"a":{"a":0,"k":[0,34],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":9,"s":[100,0],"e":[100,100]},{"t":20}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"bar 2-1","np":2,"cix":2,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-15.628,9.493],[15.628,9.493],[15.628,-9.493],[-15.628,-9.493]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.137254901961,0.207843137255,0.294117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[67.823,210.07],"ix":2},"a":{"a":0,"k":[0,9.5],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":8,"s":[100,0],"e":[100,100]},{"t":19}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"bar 1-2","np":2,"cix":2,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-15.628,22.126],[15.628,22.126],[15.628,-22.126],[-15.628,-22.126]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.274509803922,0.474509803922,0.603921568627,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[67.899,209.788],"ix":2},"a":{"a":0,"k":[0,22],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":4,"s":[100,0],"e":[100,100]},{"t":13}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"bar 1-1","np":2,"cix":2,"ix":7,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"iPhone Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155,305,0],"ix":2},"a":{"a":0,"k":[153,303,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,27],[135,27],[135,-27],[-135,-27]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.003921568627,0.376470588235,0.529411764706,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,84],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,217.5],[135,217.5],[135,-217.5],[-135,-217.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,328.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-23.1],[0,0],[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0]],"o":[[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0],[0,0],[0,-23.1],[0,0]],"v":[[-111,-303],[-153,-261],[-153,261],[-111,303],[111,303],[153,261],[153,-261],[111,-303]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.137254901961,0.207843137255,0.294117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153.242,303.477],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0}],"markers":[]} diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a7fc..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/Contents.json deleted file mode 100644 index 92b10d87b4f7..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "darkgrey-shadow.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "darkgrey-shadow@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "darkgrey-shadow@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/darkgrey-shadow.png b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/darkgrey-shadow.png deleted file mode 100644 index e3d1026dcccf427b93e518f560923d87cd570a3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 830 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=`2Jfl)d$B%&n3*T*V3KUXg?B|j-u zuOhbqD9^xPV_#8_n4FzjqL7rDo|$K>^nUk#C56lsTcvPQUjyF)=hTc$kE){7;3~h6 zMhJ1(0FE1&_nsU?XD6}dTi#a0!zN?>!XfNYSkzLEl1NlCV?QiN}Sf^&XR zs)DJWsh)w79hZVlQA(Oskc%5sGmvMilu=SrV5P5LUS6(OZmgGIl&)`RX=$l%V5Dzk zqzhD`TU?n}l31aeSF8*&0%C?sYH@N=WU*oHAIkz?GXbd+9~yC1#FZAqt9V7qSn?}@z&2)Zcx zFgUSaODP#v@jiajex6Rz72<2y>U(4Anuig*+M2q`I{L@wdWuguReNT-#iN@$ zBRg%A?!IW5d8EHPQ;p+6@`S*Tc6)d(>TADx!vE*l`3aL_T=#eh_J&Sca^kVzR)wpN zg@hL!PhF-t>+k>D;kJf0A?x0KaDF^FanIMLTJC+`Z67N$B<{N>RJU~JnA(>!D8FAc zebuaL`8m=BWsz5ttPA#W#`0%{bpP`*Gt5?ET(5p%7sK1@7xsj_i%gyWxVmG_>G0Tt z@8rImtbG27U(Y-J%;vjqE&>xKXMsm#F#`j)FbFd;%$g$sO0AwQjv*3LlM^J|8UoL& z&XA1lX>5FGZ*}(2fddB|dpdP^czE6f#PS3POHBA-VCa_Qqmp5IrWPp5!^5z7EBmjG SEpz{ZlD(&^pUXO@geCwlW~a zDsl^e@(c_%_7w$*$=RtT3Q4KynR&KK?|1K4QpilPRSGxtHSjHPPR+>ls47YguJQ{> zuF6ifOi{A8KQ26aVgjorKDK}xwt_!19`Se86_nJR{Hwo<>h+i#(Mch>H3D2mX`VkM*2oZ zx=P7{9OiaozEwNQn(g#_h548p8Tz$BE zfgHGxQ}ck{ECTvR%4_v2U@$diIy(mx2e~^bc)B{98Csf|=^E)7GB9XNES-4T+sRR+ z-F~aVDT5UYT)8>3mtJIEV&>=-qM(>|A$zg8d}IIVyc^JW~t*NW5qknv^r}&gpwP&VVJi56v zveP!{?u(Y0NBX-n)i@p`PYC>Iw}8 zSIw%HpCesR7I`(vx?mq?EPqx=_dhQ)!)ztS_39UPF}%HgVNb}r$kh3dt2@@54v#(f zPVURe%IBZ>^}N&1Y`**EA~0cc7I;J!Gcbs$f-s|Jkje+3V5z5zV~9oX+ljY%8x(k4 z{gppkv`X*zuk(YM#XI^!ex8#;t;{LbZQHgzoAh}XtAN7vxkbym`D<4#)13O$w8Ej- zLZSM={Rff(w-p@U8f}>Vy!7nE2IY)RJ=Zse=gu|wI+@1ap!DeShwC0 ce<1XO@vyc6zvXszK2Uz~boFyt=akR{0K>>)g8%>k diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/darkgrey-shadow@3x.png b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/darkgrey-shadow@3x.png deleted file mode 100644 index f324520d705eb8345627a1784a99631c2ba4fa74..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 986 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-7USV3f`bi71Ki^|4CM&(%vz$xlkv ztH>-n*TA>HIW;5GqpB!1xXLdi zxhgx^GDXSWj?1RP3TQxXYDuC(MQ%=Bu~mhw64+cTAR8pCucQE0Qj%?}6yY17;GAES zs$i;Ts%M~N$E9FXl#*r@k>g7FXt#Bv$C=6)S^`fSBQuTAW;zSx}OhpQivaGchT@w8U0PNgrg1KGYVVbM@iw z1#;j%PR#>)vk2%PDX-P9fWg$5>FgX(9OUk#;OXjYW@u?n_`reqj=3xY{wx+JKj{fnvp5jwZ)t;Gd@#yBx z$WGg&yDwU19_jDSRO5J%JR$I--5#Ec`r5Ca@c(&se!}D!*F9c>y`htqoOmp_RpIJm zA>l>GQ*evWiOS>)9u>wE1!Sj*Yi$4v-$3ui@=1*S>O>_%)p>%0m6)~(+m@Uf;T)}978-h--cWiJ*>cC zbNS3=%}tKa->}VH#C=o#X)MQr%F46i-*x<}IA f{dna){fGRAUv6(zH4D9feq``;^>bP0l+XkKsd=0f diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/email.imageset/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/email.imageset/Contents.json deleted file mode 100644 index 177c91924aa0..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/email.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "email.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true - } -} diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/email.imageset/email.pdf b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/email.imageset/email.pdf deleted file mode 100644 index 11c730458006635465da712532a515e870356653..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7180 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 diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/google@2x.png b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/google@2x.png deleted file mode 100644 index 88a86b1f22e33df9fbadf5f3cbe8cb028e99661f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1166 zcmV;91abR`P)w~t% z;j1dTb5~2qWfgOBt7|jR0x&YNun0?p#%h`FIBDwq>OU!1%Y6R@5viyoMn+avsyK#U z*x6It{OC+aXL$9Gr?;6X#Lt5OgfB-ZhPpiH(gU$FQ=Aiffzg zK1*%Kz_6OZ${nORh6+)H3me-#{K_Y2>qJ~MF>`Whne93=C>+DUz`&rO**}LEgAE-Y zeN)t$I8RW_H-w!-R)L9`lZ%m&nVFScT#{ed*;8Jq`|C3km zn#RH^C``yaW)?nvIko0~Q|IUZNOlYZ6Em~675;Q%>~Q}xzp%3>skv1^#NC%P$1pH3 zFgZm=iT8i`Z|w0JXB2Vs8JRQC3$RQ+a-4O~@Be(ue*D)DeS_g3VTs@*%(3;?fAx$HNM**dZ4C4TOaWOr$PQxN^ZP&R?%)4qnm_+Hae4lao6o?6t`1_Z zS-JwlK~T(Ff8;a+y#dpl3zu;@h$*(Fj#zWPv^xE#6!UYi2r#g2eDwjRgDib~>FFRX zac&(3)}258;c^h8hN&4n9b}~-V2j^Dnr7zobdbH0pcDQmv-0($r-Lly`K|FM>bTl^ zdOAo|giD=)xoXLBTn-ZKU2v3G2fc5!|3eJM<(Q96PX9PrnK&7k{4=vL9rNqI_NLkY zgI1XR7f|3bq*f6VV3euQ2%^ywv2M zL6Y1QHeMEC!tELcMpROzIMy8>rsW#VvX z(L6znqe#U`OHKcu?z2O2%z;d!ldQ~)`1}0qd@Q0a-P%`a;i%9RCjVDXwf_I5)#?AW zGOI^YeC*Q14Y}~Kh&r}to}`7Nf>)UQFK9NnrXCbbN~PV07*qoM6N<$f&utCX#fBK diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/google@3x.png b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/google@3x.png deleted file mode 100644 index b4d46452567dc373ce2e3c5d2ac2b2e62f9467ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1773 zcmVm@N};8cVg*VIw70y37FM2uR0I_#%5Y8) ze1O>mozWpUGNbW<2IrL5kb%>{bT~y(78H#xGMD&>njkPi=NOEDk6GdD55$RzT-w`G zc9QSUp3`5>x!?WHITwP+Y%qe*r%JtT9a1y-qJk1Py!L{pe{&lie6LRy-0{LYxN`tk zwmtR;{Pn)b`_yG8QQ;vw$CX*;mi|f@p;95EYA}LWFjSFFqP=C(pxfQvA&-DIfh$}4 zr%8&BTG>R+fnf?|!aN9KVruJPch^a!A$#wAA2aS zKOh_QW_T!WJhb5~UIsxhtXGnWnLy}<*Iv*!=*@7G{dk^c>L}0)iou?@7+1DEHZJaIwPHobk-GGF!rUDb1sWL-2JQG6>q#J?sIz!~rE#E!!aB5(OF0gUIjvb%A?fIY+QAkIhT}jLl2`lq1O6%oi1uhrwD+Gzc;9apR+ zB?G$R#~QT(iTy6#v1CT7wH?`t6&>dp(A@fZpSBJu8Kfk3rrm;MZBV@F`2`3OuP9>WETVUsWM;kirP6VSF?8Y13cnS zVyNJb7gnrU@sLqB7(viOcU!P}4KS~K20Z3=jR~`LNX>+dy}|OZaI9VfhQs8`g&+_BYWN2u1aoOuItX>1KGaA6n>jU!8w>N|sjcsrIDg;Hbng@Tq1$!CRMNffO zOxJ4_z`DFiF}J-C_Wvd9&bq+PAy{p9KcS`g^)JnnYQ_jBkD7Bj@S?35$Az--N_Y;7V$XdO-zmJ{xr z|FQ^{;bWo9>pFnyXfl{xfj>8~#Mp)|`%?V4D~Q4pJlTn_cF$OKXV+*b`;(Wztixd- zzDf?_tK>kkkh|5;uoFwAXNkg`8D_%&D*SgoXU~EA@q-|)%I6g-zDf?%rCv|Wm{gvj zP_8Vxs5e(NNCb5|+rMMvo=mC=8L|I5(+vWvGe>}4kHGc9lZJwQmng0#I$QhV+=|~k zCxt2Hw_CA#4Pe&Y1T&AQM@0>Xi<+ISZE*%?{Jw#D0*)twe5cus&d4!6ZMih9o5kkF;*hA76P9Zia zWTONO(`h%eX0(rIwl;(~Z638lo`SJxJh0)AEpyyYp&@iKP=P|L(AIT|N8>^Lu~E^_ zq+>Sk?O3_d<*YWinR_Jnxc=7jAsEu?hTp^zvy-Lp4=gB?J-xhBIj(ABW2j*4lQTMt z<`h%n*{cmMzDf?lcZhEZqiolhGN=w?9wbpR1$%>(Zny^?LHY z-{%$kgGXO|Fe<7yS2Y+moe?>%rQx2y*+K?R*H~We18!^!dM7MDa+($w?Jxx6sr+JN zz%8hhgP-;&Mxbhvy-uMBjB0tm2_f<{eu**Q;;ZCf_c#8aCtn55Etg-SQ&HW&vl1`h zn6dGzy+Dnt`Z#_SzSk^a@C_I&a5TU^2f7eK7(VP^UTpA1wQWr^kcnZcM zy;t14Kt^Izq2zacgJLV?1M)PdG80S8&TzLh7)%p{*`!7665k;kG`j+S?z2izVZk$% zOyC6@1hd3bLu9FrMKK3Fx^$VHpj?+GVU+C}ngwQST$hM2gjn*;q|Onnm9iB60cDPB zW5jN8OH7GpZ(O-l6I&@CP?yR2BlmjTSLL~0@yQSzcAIPe1zTznjnPp{GXKlJ>+5(@ P00000NkvXXu0mjfNET5! diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-password-field.imageset/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-password-field.imageset/Contents.json deleted file mode 100644 index ff89e46ad289..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-password-field.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon-password-field.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-password-field.imageset/icon-password-field.pdf b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-password-field.imageset/icon-password-field.pdf deleted file mode 100644 index d4a27e75af0967ffaf1f10eebbfa05fee0b307e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4050 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 diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-post-search-highlight.imageset/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-post-search-highlight.imageset/Contents.json deleted file mode 100644 index f8148aff0f0e..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-post-search-highlight.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon-post-search-highlight.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-post-search-highlight.imageset/icon-post-search-highlight.pdf b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-post-search-highlight.imageset/icon-post-search-highlight.pdf deleted file mode 100644 index be4a1eab88cfc0a730414ed96fd57ceb45cf4ef6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4126 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 diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/key-icon.imageset/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/key-icon.imageset/Contents.json deleted file mode 100644 index 7ee24619adaf..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/key-icon.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "key.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/key-icon.imageset/key.pdf b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/key-icon.imageset/key.pdf deleted file mode 100644 index 08fcc72ce3c50c0382e56cd56e1e4e81ac1ec990..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3395 zcmbtX$!;4*5WV+T^u>UD;IQ`v5CmA16)3S{MN*K1&NDaD}cBB?2H|oLz!=x%#5=^ z3BV$gGB^u~Oem+F=Q7hqY7v2WFHJCfEg388b$1!R$N(dyttBhyIDvWqKS5Dv<@0cC zhU1BM7U0yo29l>DK5tZF6|Uspxb{gtp{o9`K2 zzWaJ?%I#IX*)cM6iwp4mD?hoc9=AEDdJB5^^5bD$9;*6rj^DlP_s`|FtQ#)a*%@D# z8>qd`_rK=bk}uya&KIi@y#Kl{K`;r))|+CtE|2@XC=Yr4Sh7KD+&`DmLuiV8-%Ou~O&`pYN@S=D*9N=K0Ao)fPRCPJ9wBPN?UjJ{FJEFYJoz`y1 zjvG*-?e68Fq1cubp(e(_*$RGAR*tE>R)SN#rR25e?`uRzoRWq_&9aAD4N0v3T3%F} z4ScSP^2k3y(-Im#&jnGpt@1yrb@O!ORAaXtbh)dW^Yh&ox}Ayi`O%L~_mZw%=gpy_ zi}L#ye{j^RYJ((&olEz--|LF<%&FQ3N2|lJA@)#HZ8RV98F1TGc;Yq%jj#S;dFX00 zM$D4AMDeBMlr_o#z3*N+ypry^0ABR#cXm8T1$%jn{l(vnXV+mT%b;EQaW# zp~mB!5l#WsnFuC2$yFw#7g6wMU@9sT1Tf{CgiQ_gK1xEgiBd@*A;_TJh!jksK`IEE z5qw4;!Cf1z=O)w6DJi)$nGG=*gP@#2Sb$s>gp+8K0|b2$MyxjzP>}=5BQ%}CU1;GW z1o~oXtOXmvg~||gL^Pz%dt)Tg(vdmZ9WfX-@R>ZDN~+9j+!uUdGU;55iX&iWCG~XV zDFkwi7Yt@%j6(4RXp>RG;4{FK#so&Af|opI!o{Er$we!rS3G3i8XaL5SCLp;Ef}cr z!N@>Hyp7&D=mSkiIKqdd&_RS2RKYlxK-5Z*4y1b@G$s+`bgRZ>#yAyXx@sJPf)VmR zI)keyo**f9zTBY~i!e))A`+LD zY&Zy_Kmdx&B6Le^d9Y<2CgT*0nU)|KE}|}D)QAo)iYyu%1d@uX_DVZVD2h&tU=iaK zMa>{?;YfqGmd=stpoImelvWA1!lW2SELEe_0VXDriXN)dolgc20}pAyhR{)u1E?~i zl~k}CGZd>vA>d(1dXE#s1C)v}kb`C-Hw1#>c3OLAL&SJNkRnT|h6;v=l(H0MkEO>w zOUyI12A?4Z(xcb)j|f}L1PnSPGaju29YI5-(GL0KGN2n_*O@>qS=K7N(@`#ha}`lB zB1=`8UgQ7`PZzv&Amxs_A08UGYSp216xyLgOoQDKgWm zjxXty9p)5Tc8}9odso-H2Aja^*?N;>i)v@)b-Awc?~Qwfg*8}dQK)b*qD$C*nZOR% zZjVP!gR~#YD~wTWQAbYmvIlQ++N_|X!zN=jVdE73rCQV4l7hG$Os#e=biLmO(h5Z( z9Dz)Ajr%i&o+L)22)A3GR(X>@?;c-Y{&K*&i2~Iu6eH>#c=sW{=)>zClITfu zNQ01>6gu<_vhEvQ`2WLPDn@KPuXIUa<`JfWFX#Ng?n=8#v}G diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/login-magic-link.imageset/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/login-magic-link.imageset/Contents.json deleted file mode 100644 index bc416ad2f8a0..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/login-magic-link.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "login-magic-link.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/login-magic-link.imageset/login-magic-link.pdf b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/login-magic-link.imageset/login-magic-link.pdf deleted file mode 100644 index 81c37e8ea531062b80701f454d21f8b6c0356dea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 436869 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 z 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

`kdkli{nYp831J`q8 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 vb$QC(^Q(N1P9*L9OgR$McbL$}t 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

6Ej|o)nU9c0SI)L7LV*yWIKl#kPETg1%~h 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 diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/social-signup-waiting.imageset/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/social-signup-waiting.imageset/Contents.json deleted file mode 100644 index 8dc235dba4ad..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/social-signup-waiting.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "social-signup-waiting.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/social-signup-waiting.imageset/social-signup-waiting.pdf b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/social-signup-waiting.imageset/social-signup-waiting.pdf deleted file mode 100644 index 22d1f69bf5959bfed02eccecf67eeafbdccd199f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11401 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 diff --git a/Sources/WordPressAuthenticator/Resources/SupportedEmailClients/EmailClients.plist b/Sources/WordPressAuthenticator/Resources/SupportedEmailClients/EmailClients.plist deleted file mode 100644 index e085bae01f0e..000000000000 --- a/Sources/WordPressAuthenticator/Resources/SupportedEmailClients/EmailClients.plist +++ /dev/null @@ -1,18 +0,0 @@ - - - - - gmail - googlegmail:// - airmail - airmail:// - msOutlook - ms-outlook:// - spark - readdle-spark:// - yahooMail - ymail:// - fastmail - fastmail:// - - diff --git a/Sources/WordPressAuthenticator/Views/CircularImageView.swift b/Sources/WordPressAuthenticator/Views/CircularImageView.swift deleted file mode 100644 index 584c43f14567..000000000000 --- a/Sources/WordPressAuthenticator/Views/CircularImageView.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation - -/// UIImageView with a circular shape. -/// -class CircularImageView: UIImageView { - - override var frame: CGRect { - didSet { - refreshRadius() - } - } - - override func layoutSubviews() { - super.layoutSubviews() - refreshRadius() - } - - private func refreshRadius() { - layer.cornerRadius = frame.width * 0.5 - layer.masksToBounds = true - } -} diff --git a/Sources/WordPressAuthenticator/Views/LoginTextField.swift b/Sources/WordPressAuthenticator/Views/LoginTextField.swift deleted file mode 100644 index 0f05abfcf4ef..000000000000 --- a/Sources/WordPressAuthenticator/Views/LoginTextField.swift +++ /dev/null @@ -1,77 +0,0 @@ -import UIKit -import WordPressShared - -open class LoginTextField: WPWalkthroughTextField { - - /// Make a Swift-only property communicate a color to the - /// Objective-C only class, WPWalkthroughTextField. - /// - open override var secureTextEntryImageColor: UIColor! { - set { - // no-op. Usually set in Interface Builder. - } - get { - return WordPressAuthenticator.shared.style.secondaryNormalBorderColor - } - } - - open override func awakeFromNib() { - super.awakeFromNib() - backgroundColor = WordPressAuthenticator.shared.style.textFieldBackgroundColor - } - - override open func draw(_ rect: CGRect) { - if showTopLineSeparator { - guard let context = UIGraphicsGetCurrentContext() else { - return - } - - drawTopLine(rect: rect, context: context) - drawBottomLine(rect: rect, context: context) - } - } - - override open var placeholder: String? { - didSet { - guard let placeholder, - let font else { - return - } - - let attributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: WordPressAuthenticator.shared.style.placeholderColor, - .font: font - ] - attributedPlaceholder = NSAttributedString(string: placeholder, attributes: attributes) - } - } - - override open var leftViewImage: UIImage! { - set { - let newImage = newValue.imageWithTintColor(WordPressAuthenticator.shared.style.placeholderColor) - super.leftViewImage = newImage - } - get { - return super.leftViewImage - } - } - - private func drawTopLine(rect: CGRect, context: CGContext) { - drawBorderLine(from: CGPoint(x: rect.minX, y: rect.minY), to: CGPoint(x: rect.maxX, y: rect.minY), context: context) - } - - private func drawBottomLine(rect: CGRect, context: CGContext) { - drawBorderLine(from: CGPoint(x: rect.minX, y: rect.maxY), to: CGPoint(x: rect.maxX, y: rect.maxY), context: context) - } - - private func drawBorderLine(from startPoint: CGPoint, to endPoint: CGPoint, context: CGContext) { - let path = UIBezierPath() - - path.move(to: startPoint) - path.addLine(to: endPoint) - path.lineWidth = UIScreen.main.scale / 2.0 - context.addPath(path.cgPath) - context.setStrokeColor(WordPressAuthenticator.shared.style.secondaryNormalBorderColor.cgColor) - context.strokePath() - } -} diff --git a/Sources/WordPressAuthenticator/Views/SearchTableViewCell.swift b/Sources/WordPressAuthenticator/Views/SearchTableViewCell.swift deleted file mode 100644 index ba765d95ebdc..000000000000 --- a/Sources/WordPressAuthenticator/Views/SearchTableViewCell.swift +++ /dev/null @@ -1,160 +0,0 @@ -import UIKit -import WordPressShared - -// MARK: - SearchTableViewCellDelegate -// -public protocol SearchTableViewCellDelegate: AnyObject { - func startSearch(for: String) -} - -// MARK: - SearchTableViewCell -// -open class SearchTableViewCell: UITableViewCell { - - /// UITableView's Reuse Identifier - /// - public static let reuseIdentifier = "SearchTableViewCell" - - /// Search 'UITextField's reference! - /// - @IBOutlet public var textField: LoginTextField! - - /// UITextField's listener - /// - open weak var delegate: SearchTableViewCellDelegate? - - /// If `true` the search delegate callback is called as the text field is edited. - /// This class does not implement any Debouncer or assume a minimum character count because - /// each search is different. - /// - open var liveSearch: Bool = false - - /// If `true` then the user can type in spaces regularly. If `false` the whitespaces will be - /// stripped before they're entered into the field. - /// - open var allowSpaces: Bool = true - - /// Search UITextField's placeholder - /// - open var placeholder: String? { - get { - return textField.placeholder - } - set { - textField.placeholder = newValue - } - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - override open func awakeFromNib() { - super.awakeFromNib() - textField.delegate = self - textField.returnKeyType = .search - textField.contentInsets = Constants.textInsetsWithIcon - textField.accessibilityIdentifier = "Search field" - textField.leftViewImage = textField?.leftViewImage?.imageWithTintColor(WordPressAuthenticator.shared.style.placeholderColor) - - contentView.backgroundColor = WordPressAuthenticator.shared.style.viewControllerBackgroundColor - } -} - -// MARK: - Settings -// -private extension SearchTableViewCell { - enum Constants { - static let textInsetsWithIcon = WPStyleGuide.edgeInsetForLoginTextFields() - } -} - -// MARK: - UITextFieldDelegate -// -extension SearchTableViewCell: UITextFieldDelegate { - open func textFieldShouldClear(_ textField: UITextField) -> Bool { - if !liveSearch { - delegate?.startSearch(for: "") - } - - return true - } - - open func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if !liveSearch, - let searchText = textField.text { - delegate?.startSearch(for: searchText) - } - - return false - } - - open func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - let sanitizedString: String - - if allowSpaces { - sanitizedString = string - } else { - sanitizedString = string.trimmingCharacters(in: .whitespacesAndNewlines) - } - - let hasValidEdits = !sanitizedString.isEmpty || range.length > 0 - - if hasValidEdits { - guard let start = textField.position(from: textField.beginningOfDocument, offset: range.location), - let end = textField.position(from: start, offset: range.length), - let textRange = textField.textRange(from: start, to: end) else { - - // This shouldn't really happen but if it does, let's at least let the edit go through - return true - } - - textField.replace(textRange, withText: sanitizedString) - - if liveSearch { - startLiveSearch() - } - } - - return false - } - - /// Convenience method to abstract the logic that tells the delegate to start a live search. - /// - /// - Precondition: make sure you check if `liveSearch` is enabled before calling this method. - /// - private func startLiveSearch() { - guard let delegate, - let text = textField.text else { - return - } - - if text.isEmpty { - delegate.startSearch(for: "") - } else { - delegate.startSearch(for: text) - } - } -} - -// MARK: - Loader -// -public extension SearchTableViewCell { - func showLoader() { - guard let leftView = textField.leftView else { return } - let spinner = UIActivityIndicatorView(frame: leftView.frame) - addSubview(spinner) - spinner.startAnimating() - - textField.leftView?.alpha = 0 - } - - func hideLoader() { - for subview in subviews where subview is UIActivityIndicatorView { - subview.removeFromSuperview() - break - } - - textField.leftView?.alpha = 1 - } -} diff --git a/Sources/WordPressAuthenticator/Views/SearchTableViewCell.xib b/Sources/WordPressAuthenticator/Views/SearchTableViewCell.xib deleted file mode 100644 index a52ee3c47f84..000000000000 --- a/Sources/WordPressAuthenticator/Views/SearchTableViewCell.xib +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Views/SiteInfoHeaderView.swift b/Sources/WordPressAuthenticator/Views/SiteInfoHeaderView.swift deleted file mode 100644 index ee704ec72d61..000000000000 --- a/Sources/WordPressAuthenticator/Views/SiteInfoHeaderView.swift +++ /dev/null @@ -1,116 +0,0 @@ -import UIKit -import WordPressShared - -// MARK: - SiteInfoHeaderView -// -class SiteInfoHeaderView: UIView { - - // MARK: - Outlets - @IBOutlet private var titleLabel: UILabel! - @IBOutlet private var subtitleLabel: UILabel! - @IBOutlet private var blavatarImageView: UIImageView! - - // MARK: - Properties - - /// Site Title - /// - var title: String? { - get { - return titleLabel.text - } - set { - titleLabel.text = newValue - } - } - - /// Site Subtitle - /// - var subtitle: String? { - get { - return subtitleLabel.text - } - set { - subtitleLabel.text = newValue - } - } - - /// When enabled, the Subtitle won't be rendered. - /// - var subtitleIsHidden: Bool = true { - didSet { - refreshLabelStyles() - } - } - - /// When enabled, renders a border around the Blavatar. - /// - var blavatarBorderIsHidden: Bool = false { - didSet { - refreshBlavatarStyle() - } - } - - /// Returns (or sets) the Site's Blavatar Image. - /// - var blavatarImage: UIImage? { - get { - return blavatarImageView.image - } - set { - blavatarImageView.image = newValue - } - } - - /// Downloads the Blavatar Image at the specified URL. - /// - func downloadBlavatar(at path: String) { - blavatarImageView.image = .siteIconPlaceholderImage - - if let url = URL(string: path) { - blavatarImageView.downloadImage(from: url) - } - } - - // MARK: - Overridden Methods - - override func awakeFromNib() { - super.awakeFromNib() - refreshLabelStyles() - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - guard previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory else { - return - } - - refreshLabelStyles() - } -} - -// MARK: - Private -// -private extension SiteInfoHeaderView { - - func refreshLabelStyles() { - let titleWeight: UIFont.Weight = subtitleIsHidden ? .regular : .semibold - titleLabel.font = WPStyleGuide.fontForTextStyle(.subheadline, fontWeight: titleWeight) - titleLabel.textColor = WPStyleGuide.darkGrey() - - subtitleLabel.isHidden = subtitleIsHidden - subtitleLabel.font = WPStyleGuide.fontForTextStyle(.footnote) - subtitleLabel.textColor = WPStyleGuide.darkGrey() - } - - func refreshBlavatarStyle() { - if blavatarBorderIsHidden { - blavatarImageView.layer.borderWidth = 0 - blavatarImageView.tintColor = WordPressAuthenticator.shared.style.placeholderColor - } else { - blavatarImageView.layer.borderColor = WordPressAuthenticator.shared.style.instructionColor.cgColor - blavatarImageView.layer.borderWidth = 1 - blavatarImageView.tintColor = WordPressAuthenticator.shared.style.placeholderColor - } - } -} diff --git a/Sources/WordPressAuthenticator/Views/WebAuthenticationPresentationContext.swift b/Sources/WordPressAuthenticator/Views/WebAuthenticationPresentationContext.swift deleted file mode 100644 index 6569ed913ccd..000000000000 --- a/Sources/WordPressAuthenticator/Views/WebAuthenticationPresentationContext.swift +++ /dev/null @@ -1,15 +0,0 @@ -import AuthenticationServices -import Foundation - -class WebAuthenticationPresentationContext: NSObject, ASWebAuthenticationPresentationContextProviding { - let viewController: UIViewController - - init(viewController: UIViewController) { - self.viewController = viewController - super.init() - } - - func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { - return viewController.view.window! - } -} diff --git a/Sources/WordPressAuthenticator/WordPressAuthenticator.h b/Sources/WordPressAuthenticator/WordPressAuthenticator.h deleted file mode 100644 index 1fc583463b77..000000000000 --- a/Sources/WordPressAuthenticator/WordPressAuthenticator.h +++ /dev/null @@ -1,15 +0,0 @@ -#import - -//! Project version number for WordPressAuthenticator. -FOUNDATION_EXPORT double WordPressAuthenticatorVersionNumber; - -//! Project version string for WordPressAuthenticator. -FOUNDATION_EXPORT const unsigned char WordPressAuthenticatorVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - -#import -#import -#import -#import diff --git a/Tests/KeystoneTests/WordPressUnitTests.xctestplan b/Tests/KeystoneTests/WordPressUnitTests.xctestplan index 301b4c9017a8..7e1fda82d20c 100644 --- a/Tests/KeystoneTests/WordPressUnitTests.xctestplan +++ b/Tests/KeystoneTests/WordPressUnitTests.xctestplan @@ -31,13 +31,6 @@ "testRepetitionMode" : "retryOnFailure" }, "testTargets" : [ - { - "target" : { - "containerPath" : "container:WordPress.xcodeproj", - "identifier" : "4AD953BA2C21451700D0EEFA", - "name" : "WordPressAuthenticatorTests" - } - }, { "target" : { "containerPath" : "container:..\/Modules", diff --git a/Tests/WordPressAuthenticatorTests/Analytics/AnalyticsTrackerTests.swift b/Tests/WordPressAuthenticatorTests/Analytics/AnalyticsTrackerTests.swift deleted file mode 100644 index 6dc5b8eee022..000000000000 --- a/Tests/WordPressAuthenticatorTests/Analytics/AnalyticsTrackerTests.swift +++ /dev/null @@ -1,296 +0,0 @@ -import XCTest -import WordPressShared -@testable import WordPressAuthenticator - -class AnalyticsTrackerTests: XCTestCase { - - // MARK: - Expectations: Building the properties dictionary - - private func expectedProperties(source: AuthenticatorAnalyticsTracker.Source, flow: AuthenticatorAnalyticsTracker.Flow, step: AuthenticatorAnalyticsTracker.Step) -> [String: String] { - - return [ - AuthenticatorAnalyticsTracker.Property.source.rawValue: source.rawValue, - AuthenticatorAnalyticsTracker.Property.flow.rawValue: flow.rawValue, - AuthenticatorAnalyticsTracker.Property.step.rawValue: step.rawValue - ] - } - - private func expectedProperties(source: AuthenticatorAnalyticsTracker.Source, flow: AuthenticatorAnalyticsTracker.Flow, step: AuthenticatorAnalyticsTracker.Step, failure: String) -> [String: String] { - - var properties = expectedProperties(source: source, flow: flow, step: step) - properties[AuthenticatorAnalyticsTracker.Property.failure.rawValue] = failure - - return properties - } - - private func expectedProperties(source: AuthenticatorAnalyticsTracker.Source, flow: AuthenticatorAnalyticsTracker.Flow, step: AuthenticatorAnalyticsTracker.Step, click: AuthenticatorAnalyticsTracker.ClickTarget) -> [String: String] { - - var properties = expectedProperties(source: source, flow: flow, step: step) - properties[AuthenticatorAnalyticsTracker.Property.click.rawValue] = click.rawValue - - return properties - } - - /// Test that when tracking an event through the AnalyticsTracker, the backing analytics tracker - /// receives a matching event. - /// - func testBackingTracker() { - let source = AuthenticatorAnalyticsTracker.Source.reauthentication - let flow = AuthenticatorAnalyticsTracker.Flow.loginWithGoogle - let step = AuthenticatorAnalyticsTracker.Step.start - - let expectedEventName = AuthenticatorAnalyticsTracker.EventType.step.rawValue - let expectedEventProperties = self.expectedProperties(source: source, flow: flow, step: step) - let trackingIsOk = expectation(description: "The parameters of the tracking call are as expected") - - let track = { (event: AnalyticsEvent) in - if event.name == expectedEventName - && event.properties == expectedEventProperties { - - trackingIsOk.fulfill() - } - } - - let tracker = AuthenticatorAnalyticsTracker(enabled: true, track: track) - - tracker.set(source: source) - tracker.set(flow: flow) - tracker.track(step: step) - - waitForExpectations(timeout: 0.1) - } - - /// Test that tracking a failure maintains the source, flow and step from the previously recorded step. - /// - /// Ref: pbArwn-I6-p2 - /// - func testFailure() { - let source = AuthenticatorAnalyticsTracker.Source.default - let flow = AuthenticatorAnalyticsTracker.Flow.loginWithGoogle - let step = AuthenticatorAnalyticsTracker.Step.start - let failure = "some error" - - let expectedEventName = AuthenticatorAnalyticsTracker.EventType.failure.rawValue - let expectedEventProperties = self.expectedProperties(source: source, flow: flow, step: step, failure: failure) - let trackingIsOk = expectation(description: "The parameters of the tracking call are as expected") - - let track = { (event: AnalyticsEvent) in - // We'll ignore the first event and only check the properties from the failure. - if event.name == expectedEventName - && event.properties == expectedEventProperties { - - trackingIsOk.fulfill() - } - } - - let tracker = AuthenticatorAnalyticsTracker(enabled: true, track: track) - - tracker.set(source: source) - tracker.set(flow: flow) - tracker.track(step: step) - tracker.track(failure: failure) - - waitForExpectations(timeout: 0.1) - } - - /// Test that tracking a click maintains the source, flow and step from the previously recorded step. - /// - /// Ref: pbArwn-I6-p2 - /// - func testClick() { - let source = AuthenticatorAnalyticsTracker.Source.default - let flow = AuthenticatorAnalyticsTracker.Flow.loginWithGoogle - let step = AuthenticatorAnalyticsTracker.Step.start - let click = AuthenticatorAnalyticsTracker.ClickTarget.dismiss - - let expectedEventName = AuthenticatorAnalyticsTracker.EventType.interaction.rawValue - let expectedEventProperties = self.expectedProperties(source: source, flow: flow, step: step, click: click) - let trackingIsOk = expectation(description: "The parameters of the tracking call are as expected") - - let track = { (event: AnalyticsEvent) in - // We'll ignore the first event and only check the properties from the failure. - if event.name == expectedEventName - && event.properties == expectedEventProperties { - - trackingIsOk.fulfill() - } - } - - let tracker = AuthenticatorAnalyticsTracker(enabled: true, track: track) - - tracker.set(source: source) - tracker.set(flow: flow) - tracker.track(step: step) - tracker.track(click: click) - - waitForExpectations(timeout: 0.1) - } - - // MARK: - Legacy Tracking Support Tests - - /// Tests legacy tracking for a step - /// - func testStepLegacyTracking() { - let source = AuthenticatorAnalyticsTracker.Source.default - let flows: [AuthenticatorAnalyticsTracker.Flow] = [.loginWithApple, .signupWithApple, .loginWithGoogle, .signupWithGoogle, .loginWithSiteAddress] - let step = AuthenticatorAnalyticsTracker.Step.start - - let legacyTrackingExecuted = expectation(description: "The legacy tracking block was executed.") - legacyTrackingExecuted.expectedFulfillmentCount = flows.count - - let track = { (_: AnalyticsEvent) in - XCTFail() - } - - let tracker = AuthenticatorAnalyticsTracker(enabled: false, track: track) - - tracker.set(source: source) - - for flow in flows { - tracker.set(flow: flow) - tracker.track(step: step, ifTrackingNotEnabled: { - legacyTrackingExecuted.fulfill() - }) - } - - waitForExpectations(timeout: 0.1) - } - - /// Tests the new tracking for a step - /// - func testStepNewTracking() { - let source = AuthenticatorAnalyticsTracker.Source.default - let flows: [AuthenticatorAnalyticsTracker.Flow] = [.loginWithApple, .signupWithApple, .loginWithGoogle, .signupWithGoogle, .loginWithSiteAddress] - let step = AuthenticatorAnalyticsTracker.Step.start - - let legacyTrackingExecuted = expectation(description: "The legacy tracking block was executed.") - legacyTrackingExecuted.expectedFulfillmentCount = flows.count - - let track = { (_: AnalyticsEvent) in - legacyTrackingExecuted.fulfill() - } - - let tracker = AuthenticatorAnalyticsTracker(enabled: true, track: track) - - tracker.set(source: source) - - for flow in flows { - tracker.set(flow: flow) - tracker.track(step: step, ifTrackingNotEnabled: { - XCTFail() - }) - } - - waitForExpectations(timeout: 0.1) - } - - /// Tests legacy tracking for a click interaction - /// - func testClickLegacyTracking() { - let source = AuthenticatorAnalyticsTracker.Source.default - let flows: [AuthenticatorAnalyticsTracker.Flow] = [.loginWithApple, .signupWithApple, .loginWithGoogle, .signupWithGoogle, .loginWithSiteAddress] - let click = AuthenticatorAnalyticsTracker.ClickTarget.connectSite - - let legacyTrackingExecuted = expectation(description: "The legacy tracking block was executed.") - legacyTrackingExecuted.expectedFulfillmentCount = flows.count - - let track = { (_: AnalyticsEvent) in - XCTFail() - } - - let tracker = AuthenticatorAnalyticsTracker(enabled: false, track: track) - - tracker.set(source: source) - - for flow in flows { - tracker.set(flow: flow) - tracker.track(click: click, ifTrackingNotEnabled: { - legacyTrackingExecuted.fulfill() - }) - } - - waitForExpectations(timeout: 0.1) - } - - /// Tests the new tracking for a click interaction - /// - func testClickNewTracking() { - let source = AuthenticatorAnalyticsTracker.Source.default - let flows: [AuthenticatorAnalyticsTracker.Flow] = [.loginWithApple, .signupWithApple, .loginWithGoogle, .signupWithGoogle, .loginWithSiteAddress] - let click = AuthenticatorAnalyticsTracker.ClickTarget.connectSite - - let legacyTrackingExecuted = expectation(description: "The legacy tracking block was executed.") - legacyTrackingExecuted.expectedFulfillmentCount = flows.count - - let track = { (_: AnalyticsEvent) in - legacyTrackingExecuted.fulfill() - } - - let tracker = AuthenticatorAnalyticsTracker(enabled: true, track: track) - - tracker.set(source: source) - - for flow in flows { - tracker.set(flow: flow) - tracker.track(click: click, ifTrackingNotEnabled: { - XCTFail() - }) - } - - waitForExpectations(timeout: 0.1) - } - - /// Tests legacy tracking for a failure - /// - func testFailureLegacyTracking() { - let source = AuthenticatorAnalyticsTracker.Source.default - let flows: [AuthenticatorAnalyticsTracker.Flow] = [.loginWithApple, .signupWithApple, .loginWithGoogle, .signupWithGoogle, .loginWithSiteAddress] - - let legacyTrackingExecuted = expectation(description: "The legacy tracking block was executed.") - legacyTrackingExecuted.expectedFulfillmentCount = flows.count - - let track = { (_: AnalyticsEvent) in - XCTFail() - } - - let tracker = AuthenticatorAnalyticsTracker(enabled: false, track: track) - - tracker.set(source: source) - - for flow in flows { - tracker.set(flow: flow) - tracker.track(failure: "error", ifTrackingNotEnabled: { - legacyTrackingExecuted.fulfill() - }) - } - - waitForExpectations(timeout: 0.1) - } - - /// Tests the new tracking for a failure - /// - func testFailureNewTracking() { - let source = AuthenticatorAnalyticsTracker.Source.default - let flows: [AuthenticatorAnalyticsTracker.Flow] = [.loginWithApple, .signupWithApple, .loginWithGoogle, .signupWithGoogle, .loginWithSiteAddress] - - let legacyTrackingExecuted = expectation(description: "The legacy tracking block was executed.") - legacyTrackingExecuted.expectedFulfillmentCount = flows.count - - let track = { (_: AnalyticsEvent) in - legacyTrackingExecuted.fulfill() - } - - let tracker = AuthenticatorAnalyticsTracker(enabled: true, track: track) - - tracker.set(source: source) - - for flow in flows { - tracker.set(flow: flow) - tracker.track(failure: "error", ifTrackingNotEnabled: { - XCTFail() - }) - } - - waitForExpectations(timeout: 0.1) - } -} diff --git a/Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticator+TestsUtils.swift b/Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticator+TestsUtils.swift deleted file mode 100644 index 7bab55898707..000000000000 --- a/Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticator+TestsUtils.swift +++ /dev/null @@ -1,50 +0,0 @@ -@testable import WordPressAuthenticator - -extension WordPressAuthenticator { - - static func initializeForTesting() { - WordPressAuthenticator.initialize( - configuration: WordPressAuthenticatorConfiguration( - wpcomClientId: "a", - wpcomSecret: "b", - wpcomScheme: "c", - wpcomTermsOfServiceURL: URL(string: "https://w.org")!, - googleLoginClientId: "e", - googleLoginServerClientId: "f", - googleLoginScheme: "g", - userAgent: "h" - ), - style: WordPressAuthenticatorStyle( - primaryNormalBackgroundColor: .red, - primaryNormalBorderColor: .none, - primaryHighlightBackgroundColor: .orange, - primaryHighlightBorderColor: .none, - secondaryNormalBackgroundColor: .yellow, - secondaryNormalBorderColor: .green, - secondaryHighlightBackgroundColor: .blue, - secondaryHighlightBorderColor: .systemIndigo, - disabledBackgroundColor: .purple, - disabledBorderColor: .red, - primaryTitleColor: .orange, - secondaryTitleColor: .yellow, - disabledTitleColor: .green, - disabledButtonActivityIndicatorColor: .blue, - textButtonColor: .systemIndigo, - textButtonHighlightColor: .purple, - instructionColor: .red, - subheadlineColor: .orange, - placeholderColor: .yellow, - viewControllerBackgroundColor: .green, - textFieldBackgroundColor: .blue, - navBarImage: UIImage(), - navBarBadgeColor: .systemIndigo, - navBarBackgroundColor: .purple - ), - unifiedStyle: .none, - displayImages: WordPressAuthenticatorDisplayImages( - magicLink: UIImage() - ), - displayStrings: WordPressAuthenticatorDisplayStrings() - ) - } -} diff --git a/Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticatorDisplayTextTests.swift b/Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticatorDisplayTextTests.swift deleted file mode 100644 index 5773a045a4d8..000000000000 --- a/Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticatorDisplayTextTests.swift +++ /dev/null @@ -1,24 +0,0 @@ -import XCTest -@testable import WordPressAuthenticator - -// MARK: - WordPressAuthenticator Display Text Unit Tests -// -class WordPressAuthenticatorDisplayTextTests: XCTestCase { - /// Default display text instance - /// - let displayTextDefaults = WordPressAuthenticatorDisplayStrings.defaultStrings - - /// Verifies that values in defaultText are not nil - /// - func testThatDefaultTextValuesAreNotNil() { - XCTAssertNotNil(displayTextDefaults.emailLoginInstructions) - XCTAssertNotNil(displayTextDefaults.siteLoginInstructions) - } - - /// Verifies that values in defaultText are not empty strings - /// - func testThatDefaultTextValuesAreNotEmpty() { - XCTAssertFalse(displayTextDefaults.emailLoginInstructions.isEmpty) - XCTAssertFalse(displayTextDefaults.siteLoginInstructions.isEmpty) - } -} diff --git a/Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticatorTests.swift b/Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticatorTests.swift deleted file mode 100644 index 7cd454a619ff..000000000000 --- a/Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticatorTests.swift +++ /dev/null @@ -1,176 +0,0 @@ -import XCTest -@testable import WordPressAuthenticator - -// MARK: - WordPressAuthenticator Unit Tests -// -class WordPressAuthenticatorTests: XCTestCase { - let timeout = TimeInterval(3) - - override class func setUp() { - super.setUp() - - WordPressAuthenticator.initialize( - configuration: WordpressAuthenticatorProvider.wordPressAuthenticatorConfiguration(), - style: WordpressAuthenticatorProvider.wordPressAuthenticatorStyle(.random), - unifiedStyle: WordpressAuthenticatorProvider.wordPressAuthenticatorUnifiedStyle(.random) - ) - } - - func testBaseSiteURL() { - var baseURL = "testsite.wordpress.com" - var url = WordPressAuthenticator.baseSiteURL(string: "http://\(baseURL)") - XCTAssert(url == "https://\(baseURL)", "Should force https for a wpcom site having http.") - - url = WordPressAuthenticator.baseSiteURL(string: baseURL) - XCTAssert(url == "https://\(baseURL)", "Should force https for a wpcom site without a scheme.") - - baseURL = "www.selfhostedsite.com" - url = WordPressAuthenticator.baseSiteURL(string: baseURL) - XCTAssert((url == "https://\(baseURL)"), "Should add https:\\ for a non wpcom site missing a scheme.") - - url = WordPressAuthenticator.baseSiteURL(string: "\(baseURL)/wp-login.php") - XCTAssert((url == "https://\(baseURL)"), "Should remove wp-login.php from the path.") - - url = WordPressAuthenticator.baseSiteURL(string: "\(baseURL)/wp-admin") - XCTAssert((url == "https://\(baseURL)"), "Should remove /wp-admin from the path.") - - url = WordPressAuthenticator.baseSiteURL(string: "\(baseURL)/wp-admin/") - XCTAssert((url == "https://\(baseURL)"), "Should remove /wp-admin/ from the path.") - - url = WordPressAuthenticator.baseSiteURL(string: "\(baseURL)/") - XCTAssert((url == "https://\(baseURL)"), "Should remove a trailing slash from the url.") - - // Check non-latin characters and puny code - baseURL = "http://例.例" - let punycode = "http://xn--fsq.xn--fsq" - url = WordPressAuthenticator.baseSiteURL(string: baseURL) - XCTAssert(url == punycode) - url = WordPressAuthenticator.baseSiteURL(string: punycode) - XCTAssert(url == punycode) - } - - func testBaseSiteURLKeepsHTTPSchemeForNonWPSites() { - let url = "http://selfhostedsite.com" - let correctedURL = WordPressAuthenticator.baseSiteURL(string: url) - XCTAssertEqual(correctedURL, url) - } - - // MARK: View Tests - func testWordpressAuthIsAuthenticationViewController() { - let loginViewcontroller = LoginViewController() - let nuxViewController = NUXViewController() - let nuxTableViewController = NUXTableViewController() - let basicViewController = UIViewController() - - XCTAssertTrue(WordPressAuthenticator.isAuthenticationViewController(loginViewcontroller)) - XCTAssertTrue(WordPressAuthenticator.isAuthenticationViewController(nuxViewController)) - XCTAssertTrue(WordPressAuthenticator.isAuthenticationViewController(nuxTableViewController)) - XCTAssertFalse(WordPressAuthenticator.isAuthenticationViewController(basicViewController)) - } - - func testShowLoginFromPresenterReturnsLoginInitialVC() { - let presenterSpy = ModalViewControllerPresentingSpy() - let expectation = XCTNSPredicateExpectation(predicate: NSPredicate(block: { _, _ -> Bool in - return presenterSpy.presentedVC != nil - }), object: .none) - - WordPressAuthenticator.showLoginFromPresenter(presenterSpy, animated: true) - wait(for: [expectation], timeout: timeout) - - XCTAssertTrue(presenterSpy.presentedVC is LoginNavigationController) - } - - func testShowLoginForJustWPComPresentsCorrectVC() { - let presenterSpy = ModalViewControllerPresentingSpy() - let expectation = XCTNSPredicateExpectation(predicate: NSPredicate(block: { _, _ -> Bool in - return presenterSpy.presentedVC != nil - }), object: .none) - - WordPressAuthenticator.showLoginForJustWPCom(from: presenterSpy) - wait(for: [expectation], timeout: timeout) - - XCTAssertTrue(presenterSpy.presentedVC is LoginNavigationController) - } - - func testSignInForWPOrgReturnsVC() { - let vc = WordPressAuthenticator.signinForWPOrg() - - XCTAssertTrue(vc is LoginSiteAddressViewController) - } - - func testShowLoginForJustWPComSetsMetaProperties() throws { - let presenterSpy = ModalViewControllerPresentingSpy() - let expectation = XCTNSPredicateExpectation(predicate: NSPredicate(block: { _, _ -> Bool in - return presenterSpy.presentedVC != nil - }), object: .none) - - WordPressAuthenticator.showLoginForJustWPCom(from: presenterSpy, - jetpackLogin: false, - connectedEmail: "email-address@example.com") - - let navController = try XCTUnwrap(presenterSpy.presentedVC as? LoginNavigationController) - let controller = try XCTUnwrap(navController.viewControllers.first as? LoginEmailViewController) - - wait(for: [expectation], timeout: timeout) - - XCTAssertEqual(controller.loginFields.restrictToWPCom, true) - XCTAssertEqual(controller.loginFields.username, "email-address@example.com") - } - - func testShowLoginForSelfHostedSitePresentsCorrectVC() { - let presenterSpy = ModalViewControllerPresentingSpy() - let expectation = XCTNSPredicateExpectation(predicate: NSPredicate(block: { _, _ -> Bool in - return presenterSpy.presentedVC != nil - }), object: .none) - - WordPressAuthenticator.showLoginForSelfHostedSite(presenterSpy) - wait(for: [expectation], timeout: timeout) - - XCTAssertTrue(presenterSpy.presentedVC is LoginNavigationController) - } - - func testSignInForWPComWithLoginFieldsReturnsVC() throws { - let navController = try XCTUnwrap(WordPressAuthenticator.signinForWPCom(dotcomEmailAddress: "example@email.com", dotcomUsername: "username") as? UINavigationController) - let vc = navController.topViewController - - XCTAssertTrue(vc is LoginWPComViewController) - } - - func testSignInForWPComSetsEmptyLoginFields() throws { - let navController = try XCTUnwrap(WordPressAuthenticator.signinForWPCom(dotcomEmailAddress: nil, dotcomUsername: nil) as? UINavigationController) - let vc = try XCTUnwrap(navController.topViewController as? LoginWPComViewController) - - XCTAssertEqual(vc.loginFields.emailAddress, "") - XCTAssertEqual(vc.loginFields.username, "") - } - - // MARK: WordPressAuthenticator URL verification Tests - func testIsGoogleAuthURL() { - let authenticator = WordpressAuthenticatorProvider.getWordpressAuthenticator() - let googleURL = URL(string: "com.googleuserconsent.apps/82ekn2932nub23h23hn3")! - let magicLinkURL = URL(string: "https://magic-login")! - let wordpressComURL = URL(string: "https://WordPress.com")! - - XCTAssertTrue(authenticator.isGoogleAuthUrl(googleURL)) - XCTAssertFalse(authenticator.isGoogleAuthUrl(magicLinkURL)) - XCTAssertFalse(authenticator.isGoogleAuthUrl(wordpressComURL)) - } - - func testIsWordPressAuthURL() { - let authenticator = WordpressAuthenticatorProvider.getWordpressAuthenticator() - let magicLinkURL = URL(string: "https://magic-login")! - let googleURL = URL(string: "https://google.com")! - let wordpressComURL = URL(string: "https://WordPress.com")! - - XCTAssertTrue(authenticator.isWordPressAuthUrl(magicLinkURL)) - XCTAssertFalse(authenticator.isWordPressAuthUrl(googleURL)) - XCTAssertFalse(authenticator.isWordPressAuthUrl(wordpressComURL)) - } - - func testHandleWordPressAuthURLReturnsTrueOnSuccess() { - let authenticator = WordpressAuthenticatorProvider.getWordpressAuthenticator() - let url = URL(string: "https://wordpress.com/wp-login.php?token=1234567890%26action&magic-login&sr=1&signature=1234567890oienhdtsra&flow=signup") - - XCTAssertTrue(authenticator.handleWordPressAuthUrl(url!, rootViewController: UIViewController(), automatedTesting: true)) - } -} diff --git a/Tests/WordPressAuthenticatorTests/Authenticator/WordPressSourceTagTests.swift b/Tests/WordPressAuthenticatorTests/Authenticator/WordPressSourceTagTests.swift deleted file mode 100644 index d2c7d3a38834..000000000000 --- a/Tests/WordPressAuthenticatorTests/Authenticator/WordPressSourceTagTests.swift +++ /dev/null @@ -1,131 +0,0 @@ -import XCTest -import WordPressAuthenticator - -class WordPressSourceTagTests: XCTestCase { - - func testGeneralLoginSourceTag() { - let tag = WordPressSupportSourceTag.generalLogin - - XCTAssertEqual(tag.name, "generalLogin") - XCTAssertEqual(tag.origin, "origin:login-screen") - } - - func testJetpackLoginSourceTag() { - let tag = WordPressSupportSourceTag.jetpackLogin - - XCTAssertEqual(tag.name, "jetpackLogin") - XCTAssertEqual(tag.origin, "origin:jetpack-login-screen") - } - - func testLoginEmailSourceTag() { - let tag = WordPressSupportSourceTag.loginEmail - - XCTAssertEqual(tag.name, "loginEmail") - XCTAssertEqual(tag.origin, "origin:login-email") - } - - func testLoginAppleSourceTag() { - let tag = WordPressSupportSourceTag.loginApple - - XCTAssertEqual(tag.name, "loginApple") - XCTAssertEqual(tag.origin, "origin:login-apple") - } - - func testlogin2FASourceTag() { - let tag = WordPressSupportSourceTag.login2FA - - XCTAssertEqual(tag.name, "login2FA") - XCTAssertEqual(tag.origin, "origin:login-2fa") - } - - func testLoginMagicLinkSourceTag() { - let tag = WordPressSupportSourceTag.loginMagicLink - - XCTAssertEqual(tag.name, "loginMagicLink") - XCTAssertEqual(tag.origin, "origin:login-magic-link") - } - - func testSiteAddressSourceTag() { - let tag = WordPressSupportSourceTag.loginSiteAddress - - XCTAssertEqual(tag.name, "loginSiteAddress") - XCTAssertEqual(tag.origin, "origin:login-site-address") - } - - func testVerifyEmailInstructionsSourceTag() { - let tag = WordPressSupportSourceTag.verifyEmailInstructions - - XCTAssertEqual(tag.name, "verifyEmailInstructions") - XCTAssertEqual(tag.origin, "origin:login-site-address") - } - - func testLoginUsernameSourceTag() { - let tag = WordPressSupportSourceTag.loginUsernamePassword - - XCTAssertEqual(tag.name, "loginUsernamePassword") - XCTAssertEqual(tag.origin, "origin:login-username-password") - } - - func testLoginUsernamePasswordSourceTag() { - let tag = WordPressSupportSourceTag.loginWPComUsernamePassword - - XCTAssertEqual(tag.name, "loginWPComUsernamePassword") - XCTAssertEqual(tag.origin, "origin:wpcom-login-username-password") - } - - func testLoginWPComPasswordSourceTag() { - let tag = WordPressSupportSourceTag.loginWPComPassword - - XCTAssertEqual(tag.name, "loginWPComPassword") - XCTAssertEqual(tag.origin, "origin:login-wpcom-password") - } - - func testWPComSignupEmailSourceTag() { - let tag = WordPressSupportSourceTag.wpComSignupEmail - - XCTAssertEqual(tag.name, "wpComSignupEmail") - XCTAssertEqual(tag.origin, "origin:wpcom-signup-email-entry") - } - - func testWPComSignupSourceTag() { - let tag = WordPressSupportSourceTag.wpComSignup - - XCTAssertEqual(tag.name, "wpComSignup") - XCTAssertEqual(tag.origin, "origin:signup-screen") - } - - func testWPComSignupWaitingForGoogleSourceTag() { - let tag = WordPressSupportSourceTag.wpComSignupWaitingForGoogle - - XCTAssertEqual(tag.name, "wpComSignupWaitingForGoogle") - XCTAssertEqual(tag.origin, "origin:signup-waiting-for-google") - } - - func testWPComAuthGoogleSignupWaitingForGoogleSourceTag() { - let tag = WordPressSupportSourceTag.wpComAuthWaitingForGoogle - - XCTAssertEqual(tag.name, "wpComAuthWaitingForGoogle") - XCTAssertEqual(tag.origin, "origin:auth-waiting-for-google") - } - - func testWPComAuthGoogleSignupConfirmationSourceTag() { - let tag = WordPressSupportSourceTag.wpComAuthGoogleSignupConfirmation - - XCTAssertEqual(tag.name, "wpComAuthGoogleSignupConfirmation") - XCTAssertEqual(tag.origin, "origin:auth-google-signup-confirmation") - } - - func testWPComSignupMagicLinkSourceTag() { - let tag = WordPressSupportSourceTag.wpComSignupMagicLink - - XCTAssertEqual(tag.name, "wpComSignupMagicLink") - XCTAssertEqual(tag.origin, "origin:signup-magic-link") - } - - func testWPComSignupAppleSourceTag() { - let tag = WordPressSupportSourceTag.wpComSignupApple - - XCTAssertEqual(tag.name, "wpComSignupApple") - XCTAssertEqual(tag.origin, "origin:signup-apple") - } -} diff --git a/Tests/WordPressAuthenticatorTests/Credentials/CredentialsTests.swift b/Tests/WordPressAuthenticatorTests/Credentials/CredentialsTests.swift deleted file mode 100644 index 8ed2e00f6073..000000000000 --- a/Tests/WordPressAuthenticatorTests/Credentials/CredentialsTests.swift +++ /dev/null @@ -1,127 +0,0 @@ -import XCTest -@testable import WordPressAuthenticator - -class CredentialsTests: XCTestCase { - - let token = "arstdhneio123456789qwfpgjluy" - let siteURL = "https://example.com" - let username = "user123" - let password = "arstdhneio" - let xmlrpc = "https://example.com/xmlrpc.php" - - func testWordpressComCredentialsInit() { - let wpcomCredentials = WordPressComCredentials(authToken: token, - isJetpackLogin: false, - multifactor: false, - siteURL: siteURL) - - XCTAssertEqual(wpcomCredentials.authToken, token) - XCTAssertEqual(wpcomCredentials.isJetpackLogin, false) - XCTAssertEqual(wpcomCredentials.multifactor, false) - XCTAssertEqual(wpcomCredentials.siteURL, siteURL) - } - - func testWordPressComCredentialsSiteURLReturnsDefaultValue() { - let wpcomCredentials = WordPressComCredentials(authToken: token, - isJetpackLogin: false, - multifactor: false, - siteURL: "") - - let expected = "https://wordpress.com" - - XCTAssertEqual(wpcomCredentials.siteURL, expected) - } - - func testWordPressComCredentialsEquatableReturnsCorrectValue() { - let credential = WordPressComCredentials(authToken: token, - isJetpackLogin: false, - multifactor: false, - siteURL: siteURL) - let match = WordPressComCredentials(authToken: token, - isJetpackLogin: false, - multifactor: false, - siteURL: siteURL) - let differentJetpack = WordPressComCredentials(authToken: token, - isJetpackLogin: true, - multifactor: false, - siteURL: siteURL) - let differentMultifactor = WordPressComCredentials(authToken: token, - isJetpackLogin: false, - multifactor: true, - siteURL: siteURL) - let differentSiteURL = WordPressComCredentials(authToken: token, - isJetpackLogin: false, - multifactor: false, - siteURL: "") - let differentAuthToken = WordPressComCredentials(authToken: "ARSTDBVCXZ(*&^%$", - isJetpackLogin: false, - multifactor: false, - siteURL: siteURL) - - XCTAssertEqual(credential, match) - XCTAssertEqual(credential, differentJetpack) - XCTAssertEqual(credential, differentMultifactor) - XCTAssertNotEqual(credential, differentSiteURL) - XCTAssertNotEqual(credential, differentAuthToken) - } - - func testWordpressOrgCredentialsInit() { - let wporgcredentials = WordPressOrgCredentials(username: username, - password: password, - xmlrpc: xmlrpc, - options: [:]) - - XCTAssertEqual(wporgcredentials.username, username) - XCTAssertEqual(wporgcredentials.password, password) - XCTAssertEqual(wporgcredentials.xmlrpc, xmlrpc) - } - - func testWordPressOrgCredentialsEquatable() { - let lhs = WordPressOrgCredentials(username: username, - password: password, - xmlrpc: xmlrpc, - options: [:]) - - let rhs = WordPressOrgCredentials(username: username, - password: password, - xmlrpc: xmlrpc, - options: [:]) - - XCTAssertTrue(lhs == rhs) - } - - func testWordPressOrgCredentialsNotEquatable() { - let lhs = WordPressOrgCredentials(username: username, - password: password, - xmlrpc: xmlrpc, - options: [:]) - - let rhs = WordPressOrgCredentials(username: "username5678", - password: password, - xmlrpc: xmlrpc, - options: [:]) - - XCTAssertFalse(lhs == rhs) - } - - func testAuthenticatorCredentialsInit() { - let wporgCredentials = WordPressOrgCredentials(username: username, - password: password, - xmlrpc: xmlrpc, - options: [:]) - let wpcomCredentials = WordPressComCredentials(authToken: token, - isJetpackLogin: false, - multifactor: false, - siteURL: siteURL) - let authenticatorCredentials = AuthenticatorCredentials(wpcom: wpcomCredentials, - wporg: wporgCredentials) - - XCTAssertEqual(authenticatorCredentials.wpcom?.authToken, token) - XCTAssertEqual(authenticatorCredentials.wpcom?.isJetpackLogin, false) - XCTAssertEqual(authenticatorCredentials.wpcom?.multifactor, false) - XCTAssertEqual(authenticatorCredentials.wpcom?.siteURL, siteURL) - XCTAssertEqual(authenticatorCredentials.wporg?.username, username) - XCTAssertEqual(authenticatorCredentials.wporg?.password, password) - XCTAssertEqual(authenticatorCredentials.wporg?.xmlrpc, xmlrpc) - } -} diff --git a/Tests/WordPressAuthenticatorTests/EmailClientPicker/AppSelectorTests.swift b/Tests/WordPressAuthenticatorTests/EmailClientPicker/AppSelectorTests.swift deleted file mode 100644 index 350ae072e552..000000000000 --- a/Tests/WordPressAuthenticatorTests/EmailClientPicker/AppSelectorTests.swift +++ /dev/null @@ -1,74 +0,0 @@ -import XCTest -@testable import WordPressAuthenticator - -struct URLMocks { - - static let mockAppList = ["gmail": "googlemail://", "airmail": "airmail://"] -} - -class MockUrlHandler: URLHandler { - - var shouldOpenUrls = true - - var canOpenUrlExpectation: XCTestExpectation? - var openUrlExpectation: XCTestExpectation? - - func canOpenURL(_ url: URL) -> Bool { - canOpenUrlExpectation?.fulfill() - canOpenUrlExpectation = nil - return shouldOpenUrls - } - - func open(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: ((Bool) -> Void)?) { - openUrlExpectation?.fulfill() - } - - func open(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: (@MainActor @Sendable (Bool) -> Void)?) { - openUrlExpectation?.fulfill() - } -} - -class AppSelectorTests: XCTestCase { - - func testSelectorInitializationSuccess() { - // Given - let urlHandler = MockUrlHandler() - urlHandler.canOpenUrlExpectation = expectation(description: "canOpenUrl called") - // When - let appSelector = AppSelector(with: URLMocks.mockAppList, sourceView: UIView(), urlHandler: urlHandler) - // Then - XCTAssertNotNil(appSelector) - XCTAssertNotNil(appSelector?.alertController) - XCTAssertEqual(appSelector!.alertController.actions.count, 3) - waitForExpectations(timeout: 4) { error in - if let error { - XCTFail("waitForExpectationsWithTimeout errored: \(error)") - } - } - } - - func testSelectorInitializationFailsWithNoApps() { - // Given - let urlHandler = MockUrlHandler() - // When - let appSelector = AppSelector(with: [:], sourceView: UIView(), urlHandler: urlHandler) - // Then - XCTAssertNil(appSelector) - } - - func testSelectorInitializationFailsWithInvalidUrl() { - // Given - let urlHandler = MockUrlHandler() - urlHandler.canOpenUrlExpectation = expectation(description: "canOpenUrl called") - urlHandler.shouldOpenUrls = false - // When - let appSelector = AppSelector(with: URLMocks.mockAppList, sourceView: UIView(), urlHandler: urlHandler) - // Then - XCTAssertNil(appSelector) - waitForExpectations(timeout: 4) { error in - if let error { - XCTFail("waitForExpectationsWithTimeout errored: \(error)") - } - } - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/Character+URLSafeTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/Character+URLSafeTests.swift deleted file mode 100644 index 99d85c71d98e..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/Character+URLSafeTests.swift +++ /dev/null @@ -1,17 +0,0 @@ -@testable import WordPressAuthenticator -import Foundation -import XCTest - -class Character_URLSafeTests: XCTestCase { - - func testURLSafeCharacters() throws { - let urlSafe = CharacterSet(Character.urlSafeCharacters.map { "\($0)" }.joined().unicodeScalars) - - // Ensure `Character.urlSafeCharacters` is a subset of `CharacterSet.urlQueryAllowed` - XCTAssertTrue(urlSafe.isStrictSubset(of: CharacterSet.urlQueryAllowed)) - - // Notice that `CharacterSet.urlQueryAllowed` is not a subset of - // `Character.urlSafeCharacters`, though, because URL queries allow characters such as &. - XCTAssertFalse(CharacterSet.urlQueryAllowed.isStrictSubset(of: urlSafe)) - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/CodeVerifier+Fixture.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/CodeVerifier+Fixture.swift deleted file mode 100644 index 9d8a42a831cf..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/CodeVerifier+Fixture.swift +++ /dev/null @@ -1,12 +0,0 @@ -@testable import WordPressAuthenticator - -extension ProofKeyForCodeExchange.CodeVerifier { - - /// A code verifier for testing purposes that is guaranteed to be valid and deterministic. - /// - /// The reason we care about it being deterministic is because we don't want implicit randomness test. - /// The only place were we want to use random values in the `CodeVerifier` tests which explicitly check the random generation. - static func fixture() -> Self { - .init(value: (0.. String { - (0.. - - init(data: Data) { - self.init(result: .success(data)) - } - - init(error: Error) { - self.init(result: .failure(error)) - } - - init(result: Result) { - self.result = result - } - - func data(for request: URLRequest) async throws -> Data { - switch result { - case .success(let data): return data - case .failure(let error): throw error - } - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleClientIdTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleClientIdTests.swift deleted file mode 100644 index f4f73bc44ff2..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleClientIdTests.swift +++ /dev/null @@ -1,23 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class GoogleClientIdTests: XCTestCase { - - func testFailsInitIfNotAValidFormat() { - XCTAssertNil(GoogleClientId(string: "invalid")) - } - - func testDoesNotFailInitIfValidFormat() { - XCTAssertNotNil(GoogleClientId(string: "com.something.something")) - XCTAssertNotNil(GoogleClientId(string: "a.b.c")) - } - - func testRedirectURIGeneration() { - XCTAssertEqual(GoogleClientId(string: "a.b.c")?.redirectURI(path: .none), "c.b.a") - XCTAssertEqual(GoogleClientId(string: "a.b.c")?.redirectURI(path: "a_path"), "c.b.a:/a_path") - } - - func testDefaultRedirectURI() { - XCTAssertEqual(GoogleClientId(string: "a.b.c")?.defaultRedirectURI, "c.b.a:/oauth2callback") - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleOAuthTokenGetterTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleOAuthTokenGetterTests.swift deleted file mode 100644 index b17e2653e2af..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleOAuthTokenGetterTests.swift +++ /dev/null @@ -1,50 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class GoogleOAuthTokenGetterTests: XCTestCase { - - func testThrowsWhenReceivingAnError() async throws { - let dataGettingStub = DataGettingStub(error: TestError(id: 1)) - - let getter = GoogleOAuthTokenGetter(dataGetter: dataGettingStub) - - do { - _ = try await getter.getToken( - clientId: GoogleClientId(string: "a.b.c")!, - audience: "audience", - authCode: "abc", - pkce: ProofKeyForCodeExchange() - ) - XCTFail("Expected error to be thrown") - } catch { - let error = try XCTUnwrap(error as? TestError) - XCTAssertEqual(error.id, 1) - } - } - - func testReturnsTokenWhenReceivingOne() async throws { - let expectedResponse = OAuthTokenResponseBody( - accessToken: "a", - expiresIn: 1, - rawIDToken: .none, - refreshToken: .none, - scope: "s", - tokenType: "t" - ) - let dataGettingStub = DataGettingStub(data: try JSONEncoder().encode(expectedResponse)) - let getter = GoogleOAuthTokenGetter(dataGetter: dataGettingStub) - - let response = try await getter.getToken( - clientId: GoogleClientId(string: "a.b.c")!, - audience: "audience", - authCode: "abc", - pkce: ProofKeyForCodeExchange() - ) - - XCTAssertEqual(response, expectedResponse) - } -} - -struct TestError: Equatable, Error { - let id: Int -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleOAuthTokenGettingStub.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleOAuthTokenGettingStub.swift deleted file mode 100644 index 9e92ea826584..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleOAuthTokenGettingStub.swift +++ /dev/null @@ -1,30 +0,0 @@ -@testable import WordPressAuthenticator - -struct GoogleOAuthTokenGettingStub: GoogleOAuthTokenGetting { - - let result: Result - - init(response: OAuthTokenResponseBody) { - self.init(result: .success(response)) - } - - init(error: Error) { - self.init(result: .failure(error)) - } - - init(result: Result) { - self.result = result - } - - func getToken( - clientId: GoogleClientId, - audience: String, - authCode: String, - pkce: ProofKeyForCodeExchange - ) async throws -> OAuthTokenResponseBody { - switch result { - case .success(let response): return response - case .failure(let error): throw error - } - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/IDTokenTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/IDTokenTests.swift deleted file mode 100644 index f52e881fd9b6..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/IDTokenTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class IDTokenTests: XCTestCase { - - func testInitWithJWTWithoutNameNorEmailFails() throws { - XCTAssertNil(IDToken(jwt: try XCTUnwrap(JSONWebToken(encodedString: JSONWebToken.validJWTString)))) - } - - func testInitWithJWTWithoutEmailFails() throws { - XCTAssertNil(IDToken(jwt: try XCTUnwrap(JSONWebToken(encodedString: JSONWebToken.validJWTStringWithNameOnly)))) - } - - func testInitWithJWTWithoutNameFails() throws { - XCTAssertNil(IDToken(jwt: try XCTUnwrap(JSONWebToken(encodedString: JSONWebToken.validJWTStringWithEmailOnly)))) - } - - func testInitWithJWTWithNameAndEmailSucceeds() throws { - let jwt = try XCTUnwrap(JSONWebToken(encodedString: JSONWebToken.validJWTStringWithNameAndEmail)) - let token = try XCTUnwrap(IDToken(jwt: jwt)) - - XCTAssertEqual(token.name, JSONWebToken.nameFromValidJWTStringWithEmail) - XCTAssertEqual(token.email, JSONWebToken.emailFromValidJWTStringWithEmail) - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/JSONWebToken+Fixtures.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/JSONWebToken+Fixtures.swift deleted file mode 100644 index 6f5a01b6642a..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/JSONWebToken+Fixtures.swift +++ /dev/null @@ -1,58 +0,0 @@ -@testable import WordPressAuthenticator - -extension JSONWebToken { - - // Created with https://jwt.io/ with input: - // - // header: { - // "alg": "HS256", - // "typ": "JWT" - // } - // payload: { - // "key": "value", - // "other_key": "other_value" - // } - private(set) static var validJWTString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ2YWx1ZSIsIm90aGVyX2tleSI6Im90aGVyX3ZhbHVlIn0.Koc07zTGuATtQK7EvfAuwgZ-Nsr6P6J3HV4h3QLlXpM" - - // Created with https://jwt.io/ with input: - // - // header: { - // "alg": "HS256", - // "typ": "JWT" - // } - // payload: { - // "key": "value", - // "email": "test@email.com" - // } - private(set) static var validJWTStringWithEmailOnly = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ2YWx1ZSIsImVtYWlsIjoidGVzdEBlbWFpbC5jb20ifQ.b-2oTvjpc_qHM5dU6akk_ESe3eWUZwL21pvTsCmW2gE" - - // Created with https://jwt.io/ with input: - // - // header: { - // "alg": "HS256", - // "typ": "JWT" - // } - // payload: { - // "name": "John Doe", - // "key": "value" - // } - private(set) static var validJWTStringWithNameOnly = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJrZXkiOiJ2YWx1ZSJ9.P7Se5_EMlFBg5q8PV4C2IQ1YojTTSgitCBX7FgmXZzs" - - // Created with https://jwt.io/ with input: - // - // header: { - // "alg": "HS256", - // "typ": "JWT" - // } - // payload: { - // "name": "John Doe", - // "key": "value", - // "email": "test@email.com" - // } - private(set) static var validJWTStringWithNameAndEmail = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJrZXkiOiJ2YWx1ZSIsImVtYWlsIjoidGVzdEBlbWFpbC5jb20ifQ.-xzg0r5mMnSZ8hE3hk7S93iCZHhOez1QFYdheSmDlx4" - - // For convenience, this exposes the email and name value used in the fixtures. - // This allows us to use raw strings in tests, rather than having to implement encoding the JWT from an arbitrary string. - private(set) static var emailFromValidJWTStringWithEmail = "test@email.com" - private(set) static var nameFromValidJWTStringWithEmail = "John Doe" -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/JWTokenTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/JWTokenTests.swift deleted file mode 100644 index c3ea0435cf74..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/JWTokenTests.swift +++ /dev/null @@ -1,28 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class JWTokenTests: XCTestCase { - - func testJWTokenDecodingFromInvalidStringFails() { - XCTAssertNil(JSONWebToken(encodedString: "invalid")) - } - - func testJWTokenDecodingWithoutHeaderFails() { - let inputWithoutHeader = JSONWebToken.validJWTString.split(separator: ".").dropFirst().joined(separator: ".") - XCTAssertNil(JSONWebToken(encodedString: inputWithoutHeader)) - } - - func testJWTokenDecodingFromValidString() throws { - let token = try XCTUnwrap(JSONWebToken(encodedString: JSONWebToken.validJWTString)) - - XCTAssertEqual( - token.header as? [String: String], - ["alg": "HS256", "typ": "JWT"] - ) - - XCTAssertEqual( - token.payload as? [String: String], - ["key": "value", "other_key": "other_value"] - ) - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/NewGoogleAuthenticatorTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/NewGoogleAuthenticatorTests.swift deleted file mode 100644 index eebeb0d8b9b8..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/NewGoogleAuthenticatorTests.swift +++ /dev/null @@ -1,105 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class NewGoogleAuthenticatorTests: XCTestCase { - - let fakeClientId = GoogleClientId(string: "a.b.c")! - - func testRequestingOAuthTokenThrowsIfCodeCannotBeExtractedFromURL() async throws { - // Notice the use of a stub that returns a successful value. - // This way, if we get an error, we can be more confident it's legit. - let authenticator = NewGoogleAuthenticator( - clientId: fakeClientId, - scheme: "scheme", - audience: "audience", - oautTokenGetter: GoogleOAuthTokenGettingStub(response: .fixture()) - ) - let url = URL(string: "https://test.com?without=code")! - - do { - _ = try await authenticator.requestOAuthToken( - url: url, - clientId: GoogleClientId(string: "a.b.c")!, - audience: "audience", - pkce: ProofKeyForCodeExchange() - ) - XCTFail("Expected an error to be thrown") - } catch { - let error = try XCTUnwrap(error as? OAuthError) - guard case .urlDidNotContainCodeParameter(let urlFromError) = error else { - return XCTFail("Received unexpected error \(error)") - } - XCTAssertEqual(urlFromError, url) - } - } - - func testRequestingOAuthTokenRethrowsTheErrorItRecives() async throws { - let authenticator = NewGoogleAuthenticator( - clientId: fakeClientId, - scheme: "scheme", - audience: "audience", - oautTokenGetter: GoogleOAuthTokenGettingStub(error: TestError(id: 1)) - ) - let url = URL(string: "https://test.com?code=a_code")! - - do { - _ = try await authenticator.requestOAuthToken( - url: url, - clientId: GoogleClientId(string: "a.b.c")!, - audience: "audience", - pkce: ProofKeyForCodeExchange() - ) - XCTFail("Expected an error to be thrown") - } catch { - let error = try XCTUnwrap(error as? TestError) - XCTAssertEqual(error.id, 1) - } - } - - func testRequestingOAuthTokenThrowsIfIdTokenMissingFromResponse() async throws { - let authenticator = NewGoogleAuthenticator( - clientId: fakeClientId, - scheme: "scheme", - audience: "audience", - oautTokenGetter: GoogleOAuthTokenGettingStub(response: .fixture(rawIDToken: .none)) - ) - let url = URL(string: "https://test.com?code=a_code")! - - do { - _ = try await authenticator.requestOAuthToken( - url: url, - clientId: GoogleClientId(string: "a.b.c")!, - audience: "audience", - pkce: ProofKeyForCodeExchange() - ) - XCTFail("Expected an error to be thrown") - } catch { - let error = try XCTUnwrap(error as? OAuthError) - guard case .tokenResponseDidNotIncludeIdToken = error else { - return XCTFail("Received unexpected error \(error)") - } - } - } - - func testRequestingOAuthTokenReturnsTokenIfSuccessful() async throws { - let authenticator = NewGoogleAuthenticator( - clientId: fakeClientId, - scheme: "scheme", - audience: "audience", - oautTokenGetter: GoogleOAuthTokenGettingStub(response: .fixture(rawIDToken: JSONWebToken.validJWTStringWithNameAndEmail)) - ) - let url = URL(string: "https://test.com?code=a_code")! - - do { - let response = try await authenticator.requestOAuthToken( - url: url, - clientId: GoogleClientId(string: "a.b.c")!, - audience: "audience", - pkce: ProofKeyForCodeExchange() - ) - XCTAssertEqual(response.email, JSONWebToken.emailFromValidJWTStringWithEmail) - } catch { - XCTFail("Expected value, got error '\(error)'") - } - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthRequestBody+GoogleSignInTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthRequestBody+GoogleSignInTests.swift deleted file mode 100644 index ded44ae4bb31..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthRequestBody+GoogleSignInTests.swift +++ /dev/null @@ -1,22 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class OAuthRequestBodyGoogleSignInTests: XCTestCase { - - func testGoogleSignInTokenRequestBody() throws { - let codeVerifier = ProofKeyForCodeExchange.CodeVerifier.fixture() - let pkce = ProofKeyForCodeExchange(codeVerifier: codeVerifier, method: .plain) - let body = OAuthTokenRequestBody.googleSignInRequestBody( - clientId: GoogleClientId(string: "com.app.123-abc")!, - audience: "audience", - authCode: "codeValue", - pkce: pkce - ) - - XCTAssertEqual(body.clientId, "com.app.123-abc") - XCTAssertEqual(body.clientSecret, "") - XCTAssertEqual(body.codeVerifier, codeVerifier) - XCTAssertEqual(body.grantType, "authorization_code") - XCTAssertEqual(body.redirectURI, "123-abc.app.com:/oauth2callback") - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthTokenRequestBodyTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthTokenRequestBodyTests.swift deleted file mode 100644 index 917f5e75aa28..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthTokenRequestBodyTests.swift +++ /dev/null @@ -1,28 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class OAuthTokenRequestBodyTests: XCTestCase { - - func testURLEncodedDataConversion() throws { - let codeVerifier = ProofKeyForCodeExchange.CodeVerifier.fixture() - let body = OAuthTokenRequestBody( - clientId: "clientId", - clientSecret: "clientSecret", - audience: "audience", - code: "codeValue", - codeVerifier: codeVerifier, - grantType: "grantType", - redirectURI: "redirectUri" - ) - - let data = try body.asURLEncodedData() - - let decodedData = try XCTUnwrap(String(data: data, encoding: .utf8)) - - XCTAssertTrue(decodedData.contains("client_id=clientId")) - XCTAssertTrue(decodedData.contains("client_secret=clientSecret")) - XCTAssertTrue(decodedData.contains("code_verifier=\(codeVerifier.rawValue)")) - XCTAssertTrue(decodedData.contains("grant_type=grantType")) - XCTAssertTrue(decodedData.contains("redirect_uri=redirectUri")) - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthTokenResponseBody+Fixture.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthTokenResponseBody+Fixture.swift deleted file mode 100644 index 22bda7321e41..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthTokenResponseBody+Fixture.swift +++ /dev/null @@ -1,15 +0,0 @@ -@testable import WordPressAuthenticator - -extension OAuthTokenResponseBody { - - static func fixture(rawIDToken: String? = JSONWebToken.validJWTString) -> Self { - OAuthTokenResponseBody( - accessToken: "access_token", - expiresIn: 1, - rawIDToken: rawIDToken, - refreshToken: .none, - scope: "s", - tokenType: "t" - ) - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/ProofKeyForCodeExchangeTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/ProofKeyForCodeExchangeTests.swift deleted file mode 100644 index 708ea7a203ca..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/ProofKeyForCodeExchangeTests.swift +++ /dev/null @@ -1,31 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class ProofKeyForCodeExchangeTests: XCTestCase { - - func testCodeChallengeInPlainModeIsTheSameAsCodeVerifier() throws { - let codeVerifier = ProofKeyForCodeExchange.CodeVerifier.fixture() - - XCTAssertEqual( - ProofKeyForCodeExchange(codeVerifier: codeVerifier, method: .plain).codeChallenge, - codeVerifier.rawValue - ) - } - - func testCodeChallengeInS256ModeIsEncodedAsPerSpec() { - let codeVerifier = ProofKeyForCodeExchange.CodeVerifier(value: (0..<9).map { _ in "test-" }.joined())! - - XCTAssertEqual( - ProofKeyForCodeExchange(codeVerifier: codeVerifier, method: .s256).codeChallenge, - "lWvomVEGuL8FR3DY2DP_9E2q_imlqUHi-s1SPqRhO2c" - ) - } - - func testMethodURLQueryParameterValuePlain() { - XCTAssertEqual(ProofKeyForCodeExchange.Method.plain.urlQueryParameterValue, "plain") - } - - func testMethodURLQueryParameterValueS256() { - XCTAssertEqual(ProofKeyForCodeExchange.Method.s256.urlQueryParameterValue, "S256") - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/Result+ConvenienceInitTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/Result+ConvenienceInitTests.swift deleted file mode 100644 index 7b9a86064533..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/Result+ConvenienceInitTests.swift +++ /dev/null @@ -1,40 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class ResultConvenienceInitTests: XCTestCase { - - func testResultWithOptionalInputs() throws { - // Syntax sugar to keep line length shorter. SUT = System Under Test - typealias SUT = Result - - let testError = NSError(domain: "test", code: 1, userInfo: .none) - - // When value is some and error is nil, returns the value - XCTAssertEqual( - try XCTUnwrap(SUT(value: 1, error: .none, inconsistentStateError: testError).get()), - 1 - ) - - // When value is some and error is some, returns the error - let someError = NSError(domain: "test", code: 2) - XCTAssertThrowsError( - try SUT(value: 1, error: someError, inconsistentStateError: testError).get() - ) { error in - XCTAssertEqual(error as NSError, someError) - } - - // When value is none and error is some, returns the error - XCTAssertThrowsError( - try SUT(value: .none, error: someError, inconsistentStateError: testError).get() - ) { error in - XCTAssertEqual(error as NSError, someError) - } - - // When both value and error are none, returns the given error for this inconsistent state - XCTAssertThrowsError( - try SUT(value: .none, error: .none, inconsistentStateError: testError).get() - ) { error in - XCTAssertEqual(error as NSError, testError) - } - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/URL+GoogleSignInTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/URL+GoogleSignInTests.swift deleted file mode 100644 index a112065b7f95..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/URL+GoogleSignInTests.swift +++ /dev/null @@ -1,96 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class URLGoogleSignInTests: XCTestCase { - - func testGoogleSignInAuthURL() throws { - let pkce = try ProofKeyForCodeExchange() - let url = try URL.googleSignInAuthURL( - clientId: GoogleClientId(string: "123-abc245def.apps.googleusercontent.com")!, - pkce: pkce - ) - - assert(url, matchesBaseURL: "https://accounts.google.com/o/oauth2/v2/auth") - assertQueryItems( - for: url, - includeItemNamed: "client_id", - withValue: "123-abc245def.apps.googleusercontent.com" - ) - assertQueryItems( - for: url, - includeItemNamed: "code_challenge", - withValue: pkce.codeChallenge - ) - assertQueryItems( - for: url, - includeItemNamed: "code_challenge_method", - withValue: pkce.method.urlQueryParameterValue - ) - assertQueryItems( - for: url, - includeItemNamed: "redirect_uri", - withValue: "com.googleusercontent.apps.123-abc245def:/oauth2callback" - ) - assertQueryItems( - for: url, - includeItemNamed: "scope", - withValue: "profile email" - ) - assertQueryItems(for: url, includeItemNamed: "response_type", withValue: "code") - } -} - -func assert( - _ actual: URL, - matchesBaseURL baseURLString: String, - file: StaticString = #file, - line: UInt = #line -) { - guard var components = URLComponents(url: actual, resolvingAgainstBaseURL: false) else { - return XCTFail( - "Could not created `URLComponents` from given `URL` \(actual).", - file: file, - line: line - ) - } - - components.query = .none - - guard let baseURL = components.url else { - return XCTFail( - "Could not extract `URL` from `URLComponents` created from \(actual).", - file: file, - line: line - ) - } - - XCTAssertEqual(baseURL.absoluteString, baseURLString, file: file, line: line) -} - -func assertQueryItems( - for url: URL, - includeItemNamed name: String, - withValue value: String?, - file: StaticString = #file, - line: UInt = #line -) { - guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { - return XCTFail( - "Could not created `URLComponents` from given `URL` \(url).", - file: file, - line: line - ) - } - - guard let queryItems = components.queryItems else { - XCTFail("URL \(url) has no query items", file: file, line: line) - return - } - - XCTAssertTrue( - queryItems.contains(where: { $0.name == name && $0.value == value }), - "Could not find query item with name '\(name)' and value '\(value ?? "nil")'. Query items found: \(queryItems.map { "'name: \($0.name), value: \($0.value ?? "nil")'" }.joined(separator: ", "))", - file: file, - line: line - ) -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/URLRequest+GoogleSignInTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/URLRequest+GoogleSignInTests.swift deleted file mode 100644 index 384bb5556cf7..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/URLRequest+GoogleSignInTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class URLRequestOAuthTokenRequestTests: XCTestCase { - - let testBody = OAuthTokenRequestBody( - clientId: "a", - clientSecret: "b", - audience: "audience", - code: "c", - codeVerifier: ProofKeyForCodeExchange.CodeVerifier.fixture(), - grantType: "e", - redirectURI: "f" - ) - - func testURL() throws { - let request = try URLRequest.googleSignInTokenRequest(body: testBody) - XCTAssertEqual(request.url, URL(string: "https://oauth2.googleapis.com/token")!) - } - - func testMethodPost() throws { - let request = try URLRequest.googleSignInTokenRequest(body: testBody) - XCTAssertEqual(request.httpMethod, "POST") - } - - func testContentTypeFormURLEncoded() throws { - let request = try URLRequest.googleSignInTokenRequest(body: testBody) - XCTAssertEqual( - request.value(forHTTPHeaderField: "Content-Type"), - "application/x-www-form-urlencoded; charset=UTF-8" - ) - } -} diff --git a/Tests/WordPressAuthenticatorTests/Logging/LoggingTests.m b/Tests/WordPressAuthenticatorTests/Logging/LoggingTests.m deleted file mode 100644 index a0a7a5c6755f..000000000000 --- a/Tests/WordPressAuthenticatorTests/Logging/LoggingTests.m +++ /dev/null @@ -1,79 +0,0 @@ -#import - -@import WordPressAuthenticator; -@import WordPressSharedObjC; - -@interface CaptureLogs : NSObject - -@property (nonatomic, strong) NSMutableArray *infoLogs; -@property (nonatomic, strong) NSMutableArray *errorLogs; - -@end - -// We are leaving some protocol methods intentionally unimplemented to then test that calling them -// will not cause a crash. -// -// See https://github.com/wordpress-mobile/WordPressAuthenticator-iOS/pull/720#issuecomment-1374952619 -#pragma clang diagnostic ignored "-Wprotocol" -@implementation CaptureLogs - -- (instancetype)init -{ - if ((self = [super init])) { - self.infoLogs = [NSMutableArray new]; - self.errorLogs = [NSMutableArray new]; - } - return self; -} - -- (void)logInfo:(NSString *)str -{ - [self.infoLogs addObject:str]; -} - -- (void)logError:(NSString *)str -{ - [self.errorLogs addObject:str]; -} - -@end -#pragma clang diagnostic pop - -@interface ObjCLoggingTest : XCTestCase - -@property (nonatomic, strong) CaptureLogs *logger; - -@end - -@implementation ObjCLoggingTest - -- (void)setUp -{ - self.logger = [CaptureLogs new]; - WPSetLoggingDelegate(self.logger); -} - -- (void)testLogging -{ - WPLogInfo(@"This is an info log"); - WPLogInfo(@"This is an info log %@", @"with an argument"); - XCTAssertEqualObjects(self.logger.infoLogs, (@[@"This is an info log", @"This is an info log with an argument"])); - - WPLogError(@"This is an error log"); - WPLogError(@"This is an error log %@", @"with an argument"); - XCTAssertEqualObjects(self.logger.errorLogs, (@[@"This is an error log", @"This is an error log with an argument"])); -} - -- (void)testUnimplementedLoggingMethod -{ - XCTAssertNoThrow(WPLogVerbose(@"verbose logging is not implemented")); -} - -- (void)testNoLogging -{ - WPSetLoggingDelegate(nil); - XCTAssertNoThrow(WPLogInfo(@"this log should not be printed")); - XCTAssertEqual(self.logger.infoLogs.count, 0); -} - -@end diff --git a/Tests/WordPressAuthenticatorTests/Logging/LoggingTests.swift b/Tests/WordPressAuthenticatorTests/Logging/LoggingTests.swift deleted file mode 100644 index c750bf2f7c47..000000000000 --- a/Tests/WordPressAuthenticatorTests/Logging/LoggingTests.swift +++ /dev/null @@ -1,69 +0,0 @@ -import XCTest -import WordPressShared - -@testable import WordPressAuthenticator - -private class CaptureLogs: NSObject, WordPressLoggingDelegate { - var verboseLogs = [String]() - var debugLogs = [String]() - var infoLogs = [String]() - var warningLogs = [String]() - var errorLogs = [String]() - - func logError(_ str: String) { - errorLogs.append(str) - } - - func logWarning(_ str: String) { - warningLogs.append(str) - } - - func logInfo(_ str: String) { - infoLogs.append(str) - } - - func logDebug(_ str: String) { - debugLogs.append(str) - } - - func logVerbose(_ str: String) { - verboseLogs.append(str) - } -} - -class LoggingTest: XCTestCase { - - private let logger = CaptureLogs() - - override func setUp() { - WPSetLoggingDelegate(logger) - } - - func testLogging() { - WPLogVerbose("This is a verbose log") - WPLogVerbose("This is a verbose log %@", "with an argument") - XCTAssertEqual(self.logger.verboseLogs, ["This is a verbose log", "This is a verbose log with an argument"]) - - WPLogDebug("This is a debug log") - WPLogDebug("This is a debug log %@", "with an argument") - XCTAssertEqual(self.logger.debugLogs, ["This is a debug log", "This is a debug log with an argument"]) - - WPLogInfo("This is an info log") - WPLogInfo("This is an info log %@", "with an argument") - XCTAssertEqual(self.logger.infoLogs, ["This is an info log", "This is an info log with an argument"]) - - WPLogWarning("This is a warning log") - WPLogWarning("This is a warning log %@", "with an argument") - XCTAssertEqual(self.logger.warningLogs, ["This is a warning log", "This is a warning log with an argument"]) - - WPLogError("This is an error log") - WPLogError("This is an error log %@", "with an argument") - XCTAssertEqual(self.logger.errorLogs, ["This is an error log", "This is an error log with an argument"]) - } - - func testNoLogging() { - WPSetLoggingDelegate(nil) - XCTAssertNoThrow(WPLogInfo("this log should not be printed")) - XCTAssertEqual(self.logger.infoLogs.count, 0) - } -} diff --git a/Tests/WordPressAuthenticatorTests/MemoryManagementTests.swift b/Tests/WordPressAuthenticatorTests/MemoryManagementTests.swift deleted file mode 100644 index 316df137c78b..000000000000 --- a/Tests/WordPressAuthenticatorTests/MemoryManagementTests.swift +++ /dev/null @@ -1,60 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -final class MemoryManagementTests: XCTestCase { - override func setUp() { - super.setUp() - - WordPressAuthenticator.initialize( - configuration: WordpressAuthenticatorProvider.wordPressAuthenticatorConfiguration(), - style: WordpressAuthenticatorProvider.wordPressAuthenticatorStyle(.random), - unifiedStyle: WordpressAuthenticatorProvider.wordPressAuthenticatorUnifiedStyle(.random) - ) - } - - func testViewControllersDeallocatedAfterDismissing() { - let viewControllers: [UIViewController] = [ - Storyboard.login.instance.instantiateInitialViewController()!, - LoginPrologueLoginMethodViewController.instantiate(from: .login)!, - LoginPrologueSignupMethodViewController.instantiate(from: .login)!, - Login2FAViewController.instantiate(from: .login)!, - LoginEmailViewController.instantiate(from: .login)!, - LoginSelfHostedViewController.instantiate(from: .login)!, - LoginSiteAddressViewController.instantiate(from: .login)!, - LoginUsernamePasswordViewController.instantiate(from: .login)!, - LoginWPComViewController.instantiate(from: .login)!, - SignupEmailViewController.instantiate(from: .signup)!, - SignupGoogleViewController.instantiate(from: .signup)!, - GetStartedViewController.instantiate(from: .getStarted)!, - VerifyEmailViewController.instantiate(from: .verifyEmail)!, - PasswordViewController.instantiate(from: .password)!, - TwoFAViewController.instantiate(from: .twoFA)!, - GoogleAuthViewController.instantiate(from: .googleAuth)!, - SiteAddressViewController.instantiate(from: .siteAddress)!, - SiteCredentialsViewController.instantiate(from: .siteAddress)! - ] - - for viewController in viewControllers { - viewController.loadViewIfNeeded() - } - - verifyObjectsDeallocatedAfterTeardown(viewControllers) - } - - // MARK: - Helpers - - private func verifyObjectsDeallocatedAfterTeardown(_ objects: [AnyObject]) { - /// Create the array of weak objects so we could assert them in the teardown block - let weakObjects: [() -> AnyObject?] = objects.map { object in { [weak object] in - return object - } - } - - /// All the weak items should be deallocated in the teardown block unless there's a retain cycle holding them - addTeardownBlock { - for object in weakObjects { - XCTAssertNil(object(), "\(object()!.self) is not deallocated after teardown") - } - } - } -} diff --git a/Tests/WordPressAuthenticatorTests/Mocks/MockNavigationController.swift b/Tests/WordPressAuthenticatorTests/Mocks/MockNavigationController.swift deleted file mode 100644 index a0e5cadfd329..000000000000 --- a/Tests/WordPressAuthenticatorTests/Mocks/MockNavigationController.swift +++ /dev/null @@ -1,10 +0,0 @@ -import UIKit - -final class MockNavigationController: UINavigationController { - var pushedViewController: UIViewController? - - override func pushViewController(_ viewController: UIViewController, animated: Bool) { - pushedViewController = viewController - super.pushViewController(viewController, animated: true) - } -} diff --git a/Tests/WordPressAuthenticatorTests/Mocks/ModalViewControllerPresentingSpy.swift b/Tests/WordPressAuthenticatorTests/Mocks/ModalViewControllerPresentingSpy.swift deleted file mode 100644 index 01caa7da932b..000000000000 --- a/Tests/WordPressAuthenticatorTests/Mocks/ModalViewControllerPresentingSpy.swift +++ /dev/null @@ -1,8 +0,0 @@ -@testable import WordPressAuthenticator - -class ModalViewControllerPresentingSpy: UIViewController { - internal var presentedVC: UIViewController? = .none - override func present(_ viewControllerToPresent: UIViewController, animated: Bool, completion: (() -> Void)?) { - presentedVC = viewControllerToPresent - } -} diff --git a/Tests/WordPressAuthenticatorTests/Mocks/WordPressAuthenticatorDelegateSpy.swift b/Tests/WordPressAuthenticatorTests/Mocks/WordPressAuthenticatorDelegateSpy.swift deleted file mode 100644 index eb5d21b39807..000000000000 --- a/Tests/WordPressAuthenticatorTests/Mocks/WordPressAuthenticatorDelegateSpy.swift +++ /dev/null @@ -1,82 +0,0 @@ -@testable import WordPressAuthenticator -import WordPressKit -import WordPressShared - -class WordPressAuthenticatorDelegateSpy: WordPressAuthenticatorDelegate { - var dismissActionEnabled: Bool = true - var supportActionEnabled: Bool = true - var wpcomTermsOfServiceEnabled: Bool = true - var supportEnabled: Bool = true - var allowWPComLogin: Bool = true - var shouldHandleError: Bool = false - - private(set) var presentSignupEpilogueCalled = false - private(set) var socialUser: SocialUser? - - func createdWordPressComAccount(username: String, authToken: String) { - // no-op - } - - func userAuthenticatedWithAppleUserID(_ appleUserID: String) { - // no-op - } - - func presentSupportRequest(from sourceViewController: UIViewController, sourceTag: WordPressSupportSourceTag) { - // no-op - } - - func shouldPresentUsernamePasswordController(for siteInfo: WordPressComSiteInfo?, onCompletion: @escaping (WordPressAuthenticatorResult) -> Void) { - // no-op - } - - func presentLoginEpilogue(in navigationController: UINavigationController, for credentials: AuthenticatorCredentials, source: SignInSource?, onDismiss: @escaping () -> Void) { - // no-op - } - - func presentSignupEpilogue( - in navigationController: UINavigationController, - for credentials: AuthenticatorCredentials, - socialUser: SocialUser? - ) { - presentSignupEpilogueCalled = true - self.socialUser = socialUser - } - - func presentSupport(from sourceViewController: UIViewController, sourceTag: WordPressSupportSourceTag, lastStep: AuthenticatorAnalyticsTracker.Step, lastFlow: AuthenticatorAnalyticsTracker.Flow) { - // no-op - } - - func shouldPresentLoginEpilogue(isJetpackLogin: Bool) -> Bool { - true - } - - func shouldHandleError(_ error: Error) -> Bool { - shouldHandleError - } - - func handleError(_ error: Error, onCompletion: @escaping (UIViewController) -> Void) { - if shouldHandleError { - onCompletion(UIViewController()) - } - } - - func shouldPresentSignupEpilogue() -> Bool { - true - } - - func sync(credentials: AuthenticatorCredentials, onCompletion: @escaping () -> Void) { - // no-op - } - - func track(event: WPAnalyticsStat) { - // no-op - } - - func track(event: WPAnalyticsStat, properties: [AnyHashable: Any]) { - // no-op - } - - func track(event: WPAnalyticsStat, error: Error) { - // no-op - } -} diff --git a/Tests/WordPressAuthenticatorTests/Mocks/WordpressAuthenticatorProvider.swift b/Tests/WordPressAuthenticatorTests/Mocks/WordpressAuthenticatorProvider.swift deleted file mode 100644 index 578027a3d80d..000000000000 --- a/Tests/WordPressAuthenticatorTests/Mocks/WordpressAuthenticatorProvider.swift +++ /dev/null @@ -1,111 +0,0 @@ -@testable import WordPressAuthenticator - -@objc -public class WordpressAuthenticatorProvider: NSObject { - static func wordPressAuthenticatorConfiguration() -> WordPressAuthenticatorConfiguration { - return WordPressAuthenticatorConfiguration(wpcomClientId: "23456", - wpcomSecret: "arfv35dj57l3g2323", - wpcomScheme: "https", - wpcomTermsOfServiceURL: URL(string: "https://wordpress.com/tos/")!, - googleLoginClientId: "", - googleLoginServerClientId: "", - googleLoginScheme: "com.googleuserconsent.apps", - userAgent: "") - } - - static func wordPressAuthenticatorStyle(_ style: AuthenticatorStyleType) -> WordPressAuthenticatorStyle { - var wpAuthStyle: WordPressAuthenticatorStyle! - - switch style { - case .random: - wpAuthStyle = WordPressAuthenticatorStyle( - primaryNormalBackgroundColor: UIColor.random(), - primaryNormalBorderColor: UIColor.random(), - primaryHighlightBackgroundColor: UIColor.random(), - primaryHighlightBorderColor: UIColor.random(), - secondaryNormalBackgroundColor: UIColor.random(), - secondaryNormalBorderColor: UIColor.random(), - secondaryHighlightBackgroundColor: UIColor.random(), - secondaryHighlightBorderColor: UIColor.random(), - disabledBackgroundColor: UIColor.random(), - disabledBorderColor: UIColor.random(), - primaryTitleColor: UIColor.random(), - secondaryTitleColor: UIColor.random(), - disabledTitleColor: UIColor.random(), - disabledButtonActivityIndicatorColor: UIColor.random(), - textButtonColor: UIColor.random(), - textButtonHighlightColor: UIColor.random(), - instructionColor: UIColor.random(), - subheadlineColor: UIColor.random(), - placeholderColor: UIColor.random(), - viewControllerBackgroundColor: UIColor.random(), - textFieldBackgroundColor: UIColor.random(), - navBarImage: UIImage(color: UIColor.random()), - navBarBadgeColor: UIColor.random(), - navBarBackgroundColor: UIColor.random() - ) - return wpAuthStyle - } - } - - static func wordPressAuthenticatorUnifiedStyle(_ style: AuthenticatorStyleType) -> WordPressAuthenticatorUnifiedStyle { - var wpUnifiedAuthStyle: WordPressAuthenticatorUnifiedStyle! - - switch style { - case .random: - wpUnifiedAuthStyle = WordPressAuthenticatorUnifiedStyle( - borderColor: UIColor.random(), - errorColor: UIColor.random(), - textColor: UIColor.random(), - textSubtleColor: UIColor.random(), - textButtonColor: UIColor.random(), - textButtonHighlightColor: UIColor.random(), - viewControllerBackgroundColor: UIColor.random(), - navBarBackgroundColor: UIColor.random(), - navButtonTextColor: UIColor.random(), - navTitleTextColor: UIColor.random() - ) - return wpUnifiedAuthStyle - } - } - - static func getWordpressAuthenticator() -> WordPressAuthenticator { - return WordPressAuthenticator( - configuration: wordPressAuthenticatorConfiguration(), - style: wordPressAuthenticatorStyle(.random), - unifiedStyle: wordPressAuthenticatorUnifiedStyle(.random), - displayImages: WordPressAuthenticatorDisplayImages.defaultImages, - displayStrings: WordPressAuthenticatorDisplayStrings.defaultStrings) - } - - @objc(initializeWordPressAuthenticator) - public static func initializeWordPressAuthenticator() { - WordPressAuthenticator.initialize( - configuration: wordPressAuthenticatorConfiguration(), - style: wordPressAuthenticatorStyle(.random), - unifiedStyle: wordPressAuthenticatorUnifiedStyle(.random), - displayImages: WordPressAuthenticatorDisplayImages.defaultImages, - displayStrings: WordPressAuthenticatorDisplayStrings.defaultStrings) - } -} - -enum AuthenticatorStyleType { - case random -} - -extension CGFloat { - static func random() -> CGFloat { - return CGFloat(arc4random()) / CGFloat(UInt32.max) - } -} - -extension UIColor { - static func random() -> UIColor { - return UIColor( - red: .random(), - green: .random(), - blue: .random(), - alpha: 1.0 - ) - } -} diff --git a/Tests/WordPressAuthenticatorTests/Model/LoginFieldsTests.swift b/Tests/WordPressAuthenticatorTests/Model/LoginFieldsTests.swift deleted file mode 100644 index 395fed091acc..000000000000 --- a/Tests/WordPressAuthenticatorTests/Model/LoginFieldsTests.swift +++ /dev/null @@ -1,29 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class LoginFieldsTests: XCTestCase { - - func testSignInWithAppleParametersNilWhenNoSocialUser() { - XCTAssertNil(LoginFields().parametersForSignInWithApple) - } - - func testSignInWithAppleParametersNilWhenSocialUserNotApple() { - let fields = LoginFields() - fields.meta = LoginFieldsMeta( - socialUser: SocialUser(email: "email", fullName: "name", service: .google) - ) - - XCTAssertNil(fields.parametersForSignInWithApple) - } - - func testSignInWithAppleParametersHasEmailAndNameWhenSocialUserIsApple() throws { - let fields = LoginFields() - fields.meta = LoginFieldsMeta( - socialUser: SocialUser(email: "email", fullName: "name", service: .apple) - ) - - let parameters = try XCTUnwrap(fields.parametersForSignInWithApple) - XCTAssertEqual(parameters["user_email"] as? String, "email") - XCTAssertEqual(parameters["user_name"] as? String, "name") - } -} diff --git a/Tests/WordPressAuthenticatorTests/Model/LoginFieldsValidationTests.swift b/Tests/WordPressAuthenticatorTests/Model/LoginFieldsValidationTests.swift deleted file mode 100644 index 35ef3972972c..000000000000 --- a/Tests/WordPressAuthenticatorTests/Model/LoginFieldsValidationTests.swift +++ /dev/null @@ -1,42 +0,0 @@ -import XCTest -@testable import WordPressAuthenticator - -// MARK: - LoginFields Validation Tests -// -class LoginFieldsValidationTests: XCTestCase { - - func testValidateFieldsPopulatedForSignin() { - let loginFields = LoginFields() - loginFields.meta.userIsDotCom = true - - XCTAssertFalse(loginFields.validateFieldsPopulatedForSignin(), "Empty fields should not validate.") - - loginFields.username = "user" - XCTAssertFalse(loginFields.validateFieldsPopulatedForSignin(), "Should not validate with just a username") - - loginFields.password = "password" - XCTAssert(loginFields.validateFieldsPopulatedForSignin(), "should validate wpcom with username and password.") - - loginFields.meta.userIsDotCom = false - XCTAssertFalse(loginFields.validateFieldsPopulatedForSignin(), "should not validate self-hosted with just username and password.") - - loginFields.siteAddress = "example.com" - XCTAssert(loginFields.validateFieldsPopulatedForSignin(), "should validate self-hosted with username, password, and site.") - } - - func testValidateSiteForSignin() { - let loginFields = LoginFields() - - loginFields.siteAddress = "" - XCTAssertFalse(loginFields.validateSiteForSignin(), "Empty site should not validate.") - - loginFields.siteAddress = "hostname" - XCTAssertTrue(loginFields.validateSiteForSignin(), "Hostnames should validate.") - - loginFields.siteAddress = "http://hostname" - XCTAssert(loginFields.validateSiteForSignin(), "Since we want to validate simple mistakes, to use a hostname you'll need an http:// or https:// prefix.") - - loginFields.siteAddress = "https://hostname" - XCTAssert(loginFields.validateSiteForSignin(), "Since we want to validate simple mistakes, to use a hostname you'll need an http:// or https:// prefix.") - } -} diff --git a/Tests/WordPressAuthenticatorTests/Model/WordPressComSiteInfoTests.swift b/Tests/WordPressAuthenticatorTests/Model/WordPressComSiteInfoTests.swift deleted file mode 100644 index 72f955f689ca..000000000000 --- a/Tests/WordPressAuthenticatorTests/Model/WordPressComSiteInfoTests.swift +++ /dev/null @@ -1,50 +0,0 @@ -import XCTest -@testable import WordPressAuthenticator - -final class WordPressComSiteInfoTests: XCTestCase { - private var subject: WordPressComSiteInfo! - - override func setUp() { - subject = WordPressComSiteInfo(remote: mock()) - super.setUp() - } - - override func tearDown() { - super.tearDown() - subject = nil - } - - func testJetpackActiveMatchesExpectation() { - XCTAssertTrue(subject.isJetpackActive) - } - - func testHasJetpackMatchesExpectation() { - XCTAssertTrue(subject.hasJetpack) - } - - func testJetpackConnectedMatchesExpectation() { - XCTAssertTrue(subject.isJetpackConnected) - } - - func testWPComMatchesExpectation() { - XCTAssertFalse(subject.isWPCom) - } - - func testWPMatchesExpectation() { - XCTAssertTrue(subject.isWP) - } -} - -private extension WordPressComSiteInfoTests { - func mock() -> [AnyHashable: Any] { - return [ - "isJetpackActive": true, - "jetpackVersion": false, - "isWordPressDotCom": false, - "urlAfterRedirects": "https://somewhere.com", - "hasJetpack": true, - "isWordPress": true, - "isJetpackConnected": true - ] as [AnyHashable: Any] - } -} diff --git a/Tests/WordPressAuthenticatorTests/Navigation/NavigationToEnterAccountTests.swift b/Tests/WordPressAuthenticatorTests/Navigation/NavigationToEnterAccountTests.swift deleted file mode 100644 index eac10b685094..000000000000 --- a/Tests/WordPressAuthenticatorTests/Navigation/NavigationToEnterAccountTests.swift +++ /dev/null @@ -1,17 +0,0 @@ -import XCTest -@testable import WordPressAuthenticator - -final class NavigationToAccountTests: XCTestCase { - func testNavigationCommandNavigatesToExpectedDestination() { - let origin = UIViewController() - let navigationController = MockNavigationController(rootViewController: origin) - - let command = NavigateToEnterAccount(signInSource: .wpCom) - command.execute(from: origin) - - let pushedViewController = navigationController.pushedViewController - - XCTAssertNotNil(pushedViewController) - XCTAssertTrue(pushedViewController is GetStartedViewController) - } -} diff --git a/Tests/WordPressAuthenticatorTests/Navigation/NavigationToEnterSiteTests.swift b/Tests/WordPressAuthenticatorTests/Navigation/NavigationToEnterSiteTests.swift deleted file mode 100644 index 8c8452548545..000000000000 --- a/Tests/WordPressAuthenticatorTests/Navigation/NavigationToEnterSiteTests.swift +++ /dev/null @@ -1,17 +0,0 @@ -import XCTest -@testable import WordPressAuthenticator - -final class NavigationToEnterSiteTests: XCTestCase { - func testNavigationCommandNavigatesToExpectedDestination() { - let origin = UIViewController() - let navigationController = MockNavigationController(rootViewController: origin) - - let command = NavigateToEnterSite() - command.execute(from: origin) - - let pushedViewController = navigationController.pushedViewController - - XCTAssertNotNil(pushedViewController) - XCTAssertTrue(pushedViewController is SiteAddressViewController) - } -} diff --git a/Tests/WordPressAuthenticatorTests/Services/LoginFacadeTests.m b/Tests/WordPressAuthenticatorTests/Services/LoginFacadeTests.m deleted file mode 100644 index 2cd3ff932a83..000000000000 --- a/Tests/WordPressAuthenticatorTests/Services/LoginFacadeTests.m +++ /dev/null @@ -1,269 +0,0 @@ -#import "WordPressAuthenticatorTests-Swift.h" -#import "LoginFacade.h" -#import "WordPressXMLRPCAPIFacade.h" - -@import OCMock; -@import XCTest; -@import WordPressShared; -@import WordPressAuthenticator; -@import WordPressKit; - -@interface LoginFacadeTests: XCTestCase - -@property (nonatomic) LoginFacade *loginFacade; -@property (nonatomic) id mockOAuthFacade; -@property (nonatomic) id mockXMLRPCAPIFacade; -@property (nonatomic) id mockLoginFacade; -@property (nonatomic) id mockLoginFacadeDelegate; -@property (nonatomic) LoginFields *loginFields; -@property (nonatomic) NSURL *xmlrpc; -@property (nonatomic) NSMutableDictionary *xmlrpcOptions; - -@end - -@implementation LoginFacadeTests - -- (void)setUp { - [super setUp]; - - [WordpressAuthenticatorProvider initializeWordPressAuthenticator]; - - self.mockOAuthFacade = [OCMockObject niceMockForProtocol:@protocol(WordPressComOAuthClientFacadeProtocol)]; - self.mockXMLRPCAPIFacade = [OCMockObject niceMockForProtocol:@protocol(WordPressXMLRPCAPIFacade)]; - self.mockLoginFacadeDelegate = [OCMockObject niceMockForProtocol:@protocol(LoginFacadeDelegate)]; - - self.loginFacade = [LoginFacade new]; - self.loginFacade.wordpressComOAuthClientFacade = self.mockOAuthFacade; - self.loginFacade.wordpressXMLRPCAPIFacade = self.mockXMLRPCAPIFacade; - self.loginFacade.delegate = self.mockLoginFacadeDelegate; - - self.mockLoginFacade = OCMPartialMock(self.loginFacade); - OCMStub([[self.mockLoginFacade ignoringNonObjectArgs] track:0]); - OCMStub([[self.mockLoginFacade ignoringNonObjectArgs] track:0 error:[OCMArg any]]); - - self.loginFields = [LoginFields new]; - self.loginFields.username = @"username"; - self.loginFields.password = @"password"; - self.loginFields.siteAddress = @"www.mysite.com"; - self.loginFields.multifactorCode = @"123456"; -} - -- (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the class. -} - -// MARK: - WordPress.com - -- (void)testDotComExampleShouldDisplayMessageAboutConnectinToWordPressCom { - self.loginFields.userIsDotCom = YES; - - [[self.mockLoginFacadeDelegate expect] displayLoginMessage:NSLocalizedString(@"Connecting to WordPress.com", nil)]; - [self.loginFacade signInWithLoginFields:self.loginFields]; - [self.mockLoginFacadeDelegate verify]; -} - -- (void)testDotComShouldAuthenticateUserCredentials { - self.loginFields.userIsDotCom = YES; - - [[self.mockOAuthFacade expect] authenticateWithUsername:self.loginFields.username password:self.loginFields.password multifactorCode:self.loginFields.multifactorCode success:OCMOCK_ANY needsMultifactor:OCMOCK_ANY failure:OCMOCK_ANY]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockOAuthFacade verify]; -} - -- (void)testDotComShouldCallLoginFacadeDelegateFinishedLoginWithUsername { - self.loginFields.userIsDotCom = YES; - - NSString *authToken = @"auth-token"; - [OCMStub([self.mockOAuthFacade authenticateWithUsername:self.loginFields.username password:self.loginFields.password multifactorCode:self.loginFields.multifactorCode success:OCMOCK_ANY needsMultifactor:OCMOCK_ANY failure:OCMOCK_ANY]) andDo:^(NSInvocation *invocation) { - void (^ __unsafe_unretained successStub)(NSString *); - [invocation getArgument:&successStub atIndex:5]; - - successStub(authToken); - }]; - [[self.mockLoginFacadeDelegate expect] finishedLoginWithAuthToken:authToken requiredMultifactorCode:self.loginFields.requiredMultifactor]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockLoginFacadeDelegate verify]; -} - -- (void)testDotComShouldCallLoginFacadeNeedsMultifactorCodeWhenAuthentificationRequired { - self.loginFields.userIsDotCom = YES; - - [OCMStub([self.mockOAuthFacade authenticateWithUsername:self.loginFields.username password:self.loginFields.password multifactorCode:self.loginFields.multifactorCode success:OCMOCK_ANY needsMultifactor:OCMOCK_ANY failure:OCMOCK_ANY]) andDo:^(NSInvocation *invocation) { - void (^ __unsafe_unretained needsMultifactorStub)(NSInteger, SocialLogin2FANonceInfo *); - [invocation getArgument:&needsMultifactorStub atIndex:6]; - - needsMultifactorStub(0, nil); - }]; - [[self.mockLoginFacadeDelegate expect] needsMultifactorCode]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockLoginFacadeDelegate verify]; -} - -- (void)testDotComShouldCallLoginFacadeNeedsMultifactorCode { - self.loginFields.userIsDotCom = YES; - - // Expected parameters - NSInteger userID = 1234; - SocialLogin2FANonceInfo * info = [SocialLogin2FANonceInfo new]; - - // Intercept success callback and execute it when appropriate - [OCMStub([self.mockOAuthFacade authenticateWithUsername:self.loginFields.username password:self.loginFields.password multifactorCode:self.loginFields.multifactorCode success:OCMOCK_ANY needsMultifactor:OCMOCK_ANY failure:OCMOCK_ANY]) andDo:^(NSInvocation *invocation) { - void (^ __unsafe_unretained needsMultifactorStub)(NSInteger, SocialLogin2FANonceInfo *); - [invocation getArgument:&needsMultifactorStub atIndex:6]; - - needsMultifactorStub(userID, info); - }]; - [[self.mockLoginFacadeDelegate expect] needsMultifactorCodeForUserID:userID andNonceInfo:info]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockLoginFacadeDelegate verify]; -} - -- (void)testDotComShouldCallLoginFacadeDisplayRemoteError { - self.loginFields.userIsDotCom = YES; - - NSError *error = [NSError errorWithDomain:@"org.wordpress" code:-1 userInfo:@{ NSLocalizedDescriptionKey : @"Error" }]; - // Intercept success callback and execute it when appropriate - [OCMStub([self.mockOAuthFacade authenticateWithUsername:self.loginFields.username password:self.loginFields.password multifactorCode:self.loginFields.multifactorCode success:OCMOCK_ANY needsMultifactor:OCMOCK_ANY failure:OCMOCK_ANY]) andDo:^(NSInvocation *invocation) { - void (^ __unsafe_unretained failureStub)(NSError *); - [invocation getArgument:&failureStub atIndex:7]; - - failureStub(error); - }]; - [[self.mockLoginFacadeDelegate expect] displayRemoteError:error]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockLoginFacadeDelegate verify]; -} - -// MARK: - Self-Hosted - -- (void)testSelfHostedShoulDisplayAuthentificatingMessage { - self.loginFields.userIsDotCom = NO; - - [[self.mockLoginFacadeDelegate expect] displayLoginMessage:NSLocalizedString(@"Authenticating", nil)]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockLoginFacadeDelegate verify]; -} - -- (void)testSelfHostedShouldGuessingXMLRPCForSite { - self.loginFields.userIsDotCom = NO; - - [[self.mockXMLRPCAPIFacade expect] guessXMLRPCURLForSite:self.loginFields.siteAddress success:OCMOCK_ANY failure:OCMOCK_ANY]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockXMLRPCAPIFacade verify]; -} - -- (void)testSelfHostedShouldRetrieveBlogOptions { - [self mockXMLRPCFacade]; - - [[self.mockXMLRPCAPIFacade expect] getBlogOptionsWithEndpoint:self.xmlrpc username:self.loginFields.username password:self.loginFields.password success:OCMOCK_ANY failure:OCMOCK_ANY]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockXMLRPCAPIFacade verify]; -} - -- (void)testSelfHostedShouldIndicateLoginFacadeDelegateAfterRetrievingBlogOptions { - [self mockXMLRPCSuccessfulBlogOptions]; - - [[self.mockLoginFacadeDelegate expect] finishedLoginWithUsername:self.loginFields.username password:self.loginFields.password xmlrpc:[self.xmlrpc absoluteString] options:self.xmlrpcOptions]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockLoginFacadeDelegate verify]; -} - -- (void)testSelfHostedShouldAttemptAuthentificateDotComAfterRetrievingBlogOptions { - [self mockXMLRPCSuccessfulBlogOptions]; - - self.xmlrpcOptions[@"wordpress.com"] = @YES; - [[self.mockOAuthFacade expect] authenticateWithUsername:self.loginFields.username password:self.loginFields.password multifactorCode:self.loginFields.multifactorCode success:OCMOCK_ANY needsMultifactor:OCMOCK_ANY failure:OCMOCK_ANY]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockOAuthFacade verify]; -} - -- (void)testSelfHostedShouldDisplayErrorOnFailureRetrievingBlogOptions { - [self mockXMLRPCFacade]; - - NSError *error = [NSError errorWithDomain:@"org.wordpress" code:-1 userInfo:@{ NSLocalizedDescriptionKey : @"Error" }]; - - // Intercept failure callback and execute it when appropriate - [OCMStub([self.mockXMLRPCAPIFacade getBlogOptionsWithEndpoint:self.xmlrpc username:self.loginFields.username password:self.loginFields.password success:OCMOCK_ANY failure:OCMOCK_ANY]) andDo:^(NSInvocation *invocation) { - void (^ __unsafe_unretained failureStub)(NSError *); - [invocation getArgument:&failureStub atIndex:6]; - - failureStub(error); - }]; - - [[self.mockLoginFacadeDelegate expect] displayRemoteError:error]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockLoginFacadeDelegate verify]; -} - -- (void)testSelfHostedShouldDisplayErrorOnGuessXMLRPC { - self.loginFields.userIsDotCom = NO; - - NSError *error = [NSError errorWithDomain:@"org.wordpress" code:-1 userInfo:@{ NSLocalizedDescriptionKey : @"Error" }]; - - // Intercept failure callback and execute it when appropriate - [OCMStub([self.mockXMLRPCAPIFacade guessXMLRPCURLForSite:self.loginFields.siteAddress success:OCMOCK_ANY failure:OCMOCK_ANY]) andDo:^(NSInvocation *invocation) { - void (^ __unsafe_unretained failureStub)(NSError *); - [invocation getArgument:&failureStub atIndex:4]; - - failureStub(error); - }]; - - [[self.mockLoginFacadeDelegate expect] displayRemoteError:error]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockLoginFacadeDelegate verify]; -} - -// MARK: - Mocks - -- (void)mockXMLRPCFacade { - self.loginFields.userIsDotCom = NO; - - self.xmlrpc = [NSURL URLWithString:@"http://www.selfhosted.com/xmlrpc.php"]; - // Intercept success callback and execute it when appropriate - [OCMStub([self.mockXMLRPCAPIFacade guessXMLRPCURLForSite:self.loginFields.siteAddress success:OCMOCK_ANY failure:OCMOCK_ANY]) andDo:^(NSInvocation *invocation) { - void (^ __unsafe_unretained successStub)(NSURL *); - [invocation getArgument:&successStub atIndex:3]; - - successStub(self.xmlrpc); - }]; -} - -- (void)mockXMLRPCSuccessfulBlogOptions { - [self mockXMLRPCFacade]; - - self.xmlrpcOptions = [NSMutableDictionary dictionaryWithDictionary:@{@"software_version":@{@"value":@"4.2"}}]; - - // Intercept success callback and execute it when appropriate - [OCMStub([self.mockXMLRPCAPIFacade getBlogOptionsWithEndpoint:self.xmlrpc username:self.loginFields.username password:self.loginFields.password success:OCMOCK_ANY failure:OCMOCK_ANY]) andDo:^(NSInvocation *invocation) { - void (^ __unsafe_unretained successStub)(NSDictionary *); - [invocation getArgument:&successStub atIndex:5]; - - successStub(self.xmlrpcOptions); - }]; -} - -@end diff --git a/Tests/WordPressAuthenticatorTests/SingIn/AppleAuthenticatorTests.swift b/Tests/WordPressAuthenticatorTests/SingIn/AppleAuthenticatorTests.swift deleted file mode 100644 index 88ea942e0081..000000000000 --- a/Tests/WordPressAuthenticatorTests/SingIn/AppleAuthenticatorTests.swift +++ /dev/null @@ -1,107 +0,0 @@ -import AuthenticationServices -@testable import WordPressAuthenticator -import XCTest - -class AppleAuthenticatorTests: XCTestCase { - - // showSignupEpilogue with loginFields.meta.appleUser set will pass SocialService.apple to the delegate - func testShowingSignupEpilogueWithApple() throws { - WordPressAuthenticator.initializeForTesting() - let delegateSpy = WordPressAuthenticatorDelegateSpy() - WordPressAuthenticator.shared.delegate = delegateSpy - - // This might be unnecessary because delegateSpy should be deallocated once the test method finished. - // Leaving it here, just in case. - addTeardownBlock { - WordPressAuthenticator.shared.delegate = nil - } - - let socialUserCreatingStub = SocialUserCreatingStub(appleResult: .success((true, true, true, "a", "b"))) - let sut = AppleAuthenticator(signupService: socialUserCreatingStub) - - // Before acting on the SUT, we need to ensure the login fields are set as we expect - let presenterViewController = UIViewController() - // We need to create this because it's accessed by showFrom(viewController:) - _ = UINavigationController(rootViewController: presenterViewController) - sut.showFrom(viewController: presenterViewController) - sut.createWordPressComUser( - appleUserId: "apple-user-id", - email: "test@email.com", - name: "Full Name", - token: "abcd" - ) - - sut.showSignupEpilogue(for: AuthenticatorCredentials()) - - let service = try XCTUnwrap(delegateSpy.socialUser?.service) - guard case .apple = service else { - return XCTFail("Expected Apple social service, got \(service) instead") - } - } - - // showSignupEpilogue with loginFields.meta.appleUser set will not pass SocialService.apple to the delegate - func testShowingSignupEpilogueWithoutAppleUser() throws { - WordPressAuthenticator.initializeForTesting() - let delegateSpy = WordPressAuthenticatorDelegateSpy() - WordPressAuthenticator.shared.delegate = delegateSpy - - // This might be unnecessary because delegateSpy should be deallocated once the test method finished. - // Leaving it here, just in case. - addTeardownBlock { - WordPressAuthenticator.shared.delegate = nil - } - - let sut = AppleAuthenticator(signupService: SocialUserCreatingStub()) - - // Before acting on the SUT, we need to ensure the login fields are set as we expect - let presenterViewController = UIViewController() - // We need to create this because it's accessed by showFrom(viewController:) - _ = UINavigationController(rootViewController: presenterViewController) - sut.showFrom(viewController: presenterViewController) - - sut.showSignupEpilogue(for: AuthenticatorCredentials()) - - // The delegate is called, but without social service. - // - // By the way, the type system and runtime allow this to happen, but does it actually - // make sense? Not so sure. How can we callback from Sign In with Apple without the - // matching social service? - XCTAssertTrue(delegateSpy.presentSignupEpilogueCalled) - XCTAssertNil(delegateSpy.socialUser) - } -} - -// This doesn't live in a dedicated file because we currently only need it for this test. -class SocialUserCreatingStub: SocialUserCreating { - - // is new account, user name, WPCom token - private let googleResult: Result<(Bool, String, String), Error> - // is new account, existing non-social account, existing MFA account, user name, WPCom token - private let appleResult: Result<(Bool, Bool, Bool, String, String), Error> - - init( - appleResult: Result<(Bool, Bool, Bool, String, String), Error> = .failure(TestError(id: 1)), - googleResult: Result<(Bool, String, String), Error> = .failure(TestError(id: 2)) - ) { - self.appleResult = appleResult - self.googleResult = googleResult - } - - func createWPComUserWithGoogle(token: String, success: @escaping (Bool, String, String) -> Void, failure: @escaping (Error) -> Void) { - switch googleResult { - case .success((let isNewAccount, let userName, let wpComToken)): - success(isNewAccount, userName, wpComToken) - case .failure(let error): - failure(error) - } - } - - func createWPComUserWithApple(token: String, email: String, fullName: String?, success: @escaping (Bool, Bool, Bool, String, String) -> Void, failure: @escaping (Error) -> Void) { - switch appleResult { - case .success((let isNewAccount, let existingNonSocialAccount, let existing2FAAccount, let username, let wpComToken)): - success(isNewAccount, existingNonSocialAccount, existing2FAAccount, username, wpComToken) - case .failure(let error): - failure(error) - } - } -} diff --git a/Tests/WordPressAuthenticatorTests/SingIn/LoginViewControllerTests.swift b/Tests/WordPressAuthenticatorTests/SingIn/LoginViewControllerTests.swift deleted file mode 100644 index c71c8012d834..000000000000 --- a/Tests/WordPressAuthenticatorTests/SingIn/LoginViewControllerTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class LoginViewControllerTests: XCTestCase { - - // showSignupEpilogue with loginFields.meta.appleUser set will pass SocialService.apple to - // the delegate - func testShowingSignupEpilogueWithGoogleUser() throws { - WordPressAuthenticator.initializeForTesting() - let delegateSpy = WordPressAuthenticatorDelegateSpy() - WordPressAuthenticator.shared.delegate = delegateSpy - - // This might be unnecessary because delegateSpy should be deallocated once the test method finished. - // Leaving it here, just in case. - addTeardownBlock { - WordPressAuthenticator.shared.delegate = nil - } - - let sut = LoginViewController() - // We need to embed the SUT in a navigation controller because it expects its - // navigationController property to not be nil. - _ = UINavigationController(rootViewController: sut) - - sut.loginFields.meta.socialUser = SocialUser(email: "test@email.com", fullName: "Full Name", service: .google) - - sut.showSignupEpilogue(for: AuthenticatorCredentials()) - - let service = try XCTUnwrap(delegateSpy.socialUser?.service) - guard case .google = service else { - return XCTFail("Expected Google social service, got \(service) instead") - } - } -} diff --git a/Tests/WordPressAuthenticatorTests/SingIn/SiteAddressViewModelTests.swift b/Tests/WordPressAuthenticatorTests/SingIn/SiteAddressViewModelTests.swift deleted file mode 100644 index d74ba5017f9d..000000000000 --- a/Tests/WordPressAuthenticatorTests/SingIn/SiteAddressViewModelTests.swift +++ /dev/null @@ -1,125 +0,0 @@ -import XCTest -import WordPressKit -@testable import WordPressAuthenticator - -final class SiteAddressViewModelTests: XCTestCase { - private var isSiteDiscovery: Bool! - private var xmlrpcFacade: MockWordPressXMLRPCAPIFacade! - private var authenticationDelegateSpy: WordPressAuthenticatorDelegateSpy! - private var blogService: MockWordPressComBlogService! - private var loginFields: LoginFields! - private var viewModel: SiteAddressViewModel! - - override func setUp() { - super.setUp() - isSiteDiscovery = false - xmlrpcFacade = MockWordPressXMLRPCAPIFacade() - authenticationDelegateSpy = WordPressAuthenticatorDelegateSpy() - blogService = MockWordPressComBlogService() - loginFields = LoginFields() - - WordPressAuthenticator.initializeForTesting() - - viewModel = SiteAddressViewModel(isSiteDiscovery: isSiteDiscovery, xmlrpcFacade: xmlrpcFacade, authenticationDelegate: authenticationDelegateSpy, blogService: blogService, loginFields: loginFields) - } - - func testGuessXMLRPCURLSuccess() { - xmlrpcFacade.success = true - var result: SiteAddressViewModel.GuessXMLRPCURLResult? - viewModel.guessXMLRPCURL(for: "https://wordpress.com", loading: { _ in }) { res in - result = res - } - - XCTAssertEqual(result, .success) - } - - func testGuessXMLRPCURLError() { - xmlrpcFacade.error = NSError(domain: "SomeDomain", code: 1, userInfo: nil) - var result: SiteAddressViewModel.GuessXMLRPCURLResult? - viewModel.guessXMLRPCURL(for: "https://error.com", loading: { _ in }) { res in - result = res - } - if case .error(let error, _) = result { - XCTAssertEqual(error.code, 1) - } else { - XCTFail("Unexpected result: \(String(describing: result))") - } - } - - func testGuessXMLRPCURLErrorInvalidNotWP() { - xmlrpcFacade.error = WordPressOrgXMLRPCValidatorError.invalid as NSError - blogService.isWP = false - var result: SiteAddressViewModel.GuessXMLRPCURLResult? - viewModel.guessXMLRPCURL(for: "https://invalid.com", loading: { _ in }) { res in - result = res - } - - if case .error(let error, _) = result { - XCTAssertEqual(error.code, WordPressOrgXMLRPCValidatorError.invalid.rawValue) - } else { - XCTFail("Unexpected result: \(String(describing: result))") - } - } - - func testGuessXMLRPCURLErrorInvalidIsWP() { - xmlrpcFacade.error = WordPressOrgXMLRPCValidatorError.invalid as NSError - blogService.isWP = true - var result: SiteAddressViewModel.GuessXMLRPCURLResult? - viewModel.guessXMLRPCURL(for: "https://invalidwp.com", loading: { _ in }) { res in - result = res - } - if case .error(let error, _) = result { - XCTAssertEqual(error.code, WordPressOrgXMLRPCValidatorError.xmlrpc_missing.rawValue) - } else { - XCTFail("Unexpected result: \(String(describing: result))") - } - } - - func testGuessXMLRPCTroubleshootSite() { - viewModel = SiteAddressViewModel(isSiteDiscovery: true, xmlrpcFacade: xmlrpcFacade, authenticationDelegate: authenticationDelegateSpy, blogService: blogService, loginFields: loginFields) - xmlrpcFacade.error = NSError(domain: "SomeDomain", code: 1, userInfo: nil) - var result: SiteAddressViewModel.GuessXMLRPCURLResult? - viewModel.guessXMLRPCURL(for: "https://troubleshoot.com", loading: { _ in }) { res in - result = res - } - XCTAssertEqual(result, .troubleshootSite) - } - - func testGuessXMLRPCURLErrorHandledByDelegate() { - xmlrpcFacade.error = NSError(domain: "SomeDomain", code: 1, userInfo: nil) - authenticationDelegateSpy.shouldHandleError = true - - var result: SiteAddressViewModel.GuessXMLRPCURLResult? - viewModel.guessXMLRPCURL(for: "https://delegatehandles.com", loading: { _ in }) { res in - result = res - } - - if case .customUI = result { - XCTAssertTrue(true) - } else { - XCTFail("Unexpected result: \(String(describing: result))") - } - } -} - -private class MockWordPressXMLRPCAPIFacade: WordPressXMLRPCAPIFacade { - var success: Bool = false - var error: NSError? - - override func guessXMLRPCURL(forSite siteAddress: String, success: @escaping (URL?) -> (), failure: @escaping (Error?) -> ()) { - if self.success { - success(URL(string: "https://successful.site")) - } else { - failure(self.error) - } - } -} - -private class MockWordPressComBlogService: WordPressComBlogService { - var isWP = false - - override func fetchUnauthenticatedSiteInfoForAddress(for address: String, success: @escaping (WordPressComSiteInfo) -> Void, failure: @escaping (Error) -> Void) { - let siteInfo = WordPressComSiteInfo(remote: ["isWordPress": isWP]) - success(siteInfo) - } -} diff --git a/Tests/WordPressAuthenticatorTests/WordPressAuthenticator.xctestplan b/Tests/WordPressAuthenticatorTests/WordPressAuthenticator.xctestplan deleted file mode 100644 index ad7dd91d5a76..000000000000 --- a/Tests/WordPressAuthenticatorTests/WordPressAuthenticator.xctestplan +++ /dev/null @@ -1,24 +0,0 @@ -{ - "configurations" : [ - { - "id" : "EBC0D38A-5F0A-4C69-BBED-1B55B7711736", - "name" : "Configuration 1", - "options" : { - - } - } - ], - "defaultOptions" : { - - }, - "testTargets" : [ - { - "target" : { - "containerPath" : "container:WordPress.xcodeproj", - "identifier" : "4AD953BA2C21451700D0EEFA", - "name" : "WordPressAuthenticatorTests" - } - } - ], - "version" : 1 -} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 0e60ef2e2901..bec4b3a95dca 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -1041,11 +1041,6 @@ path = Sources; sourceTree = ""; }; - 0C238F782D9ADF0200981631 /* WordPressAuthenticator */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = WordPressAuthenticator; - sourceTree = ""; - }; 0C3313B82E0439A8000C3760 /* Miniature */ = { isa = PBXFileSystemSynchronizedRootGroup; path = Miniature; @@ -1080,11 +1075,6 @@ path = JetpackStatsWidgets; sourceTree = ""; }; - 0C5A1A042D9B080900C25301 /* WordPressAuthenticatorTests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = WordPressAuthenticatorTests; - sourceTree = ""; - }; 0C5A3FAB2D9B1EF400C25301 /* Reader */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( @@ -1425,7 +1415,6 @@ children = ( 0C73A9BD2DAEDFDE00CC0F3A /* KeystoneTests */, 0C3313C62E0439A9000C3760 /* MiniatureTests */, - 0C5A1A042D9B080900C25301 /* WordPressAuthenticatorTests */, 3F7AE0C22D9B30A200AB4892 /* WordPressDataTests */, 4A8280FE2E5FE9B60037E180 /* WordPressKitTests */, ); @@ -1441,7 +1430,6 @@ 0C5A3FAB2D9B1EF400C25301 /* Reader */, 0C5C46FE2D98397A00F2CD55 /* Keystone */, 0C3313B82E0439A8000C3760 /* Miniature */, - 0C238F782D9ADF0200981631 /* WordPressAuthenticator */, 3F7AE0B62D9B30A100AB4892 /* WordPressData */, 0C3E79892DB164B3000C7072 /* JetpackStatsWidgets */, ); diff --git a/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/WordPress.xcscheme b/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/WordPress.xcscheme index a6b1da6f3212..f98f3e373927 100644 --- a/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/WordPress.xcscheme +++ b/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/WordPress.xcscheme @@ -54,17 +54,6 @@ ReferencedContainer = "container:WordPress.xcodeproj"> - - - - diff --git a/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/WordPressAuthenticator.xcscheme b/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/WordPressAuthenticator.xcscheme deleted file mode 100644 index dcb72daccf6b..000000000000 --- a/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/WordPressAuthenticator.xcscheme +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/fastlane/lanes/localization.rb b/fastlane/lanes/localization.rb index ae5c693c6b71..e29a0f9c8876 100644 --- a/fastlane/lanes/localization.rb +++ b/fastlane/lanes/localization.rb @@ -194,7 +194,6 @@ def generate_strings_file(gutenberg_path:, derived_data_path:) paths: [ 'WordPress/', 'Modules/Sources/', - 'Sources/WordPressAuthenticator', gutenberg_path, *REMOTE_SWIFT_PACKAGES_TO_LOCALIZE.map { |name| File.join(derived_data_path, 'SourcePackages', 'checkouts', name, 'Sources') } ], From 3c37b488ae6caea2a54780b2f85cb04c5046d78d Mon Sep 17 00:00:00 2001 From: Tony Li Date: Wed, 10 Jun 2026 23:02:01 +1200 Subject: [PATCH 3/3] Clean up follow-ups from the authenticator removal review Refreshes a stale comment about the sign-in notification and uses WordPressKit's error type instead of a hand-built NSError. --- .../Login/SelfHostedSiteAuthenticator.swift | 7 +- .../Login/WordPressDotComAuthenticator.swift | 178 +++++++++++++----- 2 files changed, 137 insertions(+), 48 deletions(-) diff --git a/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift b/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift index 7f91da481216..d05383d4f0cb 100644 --- a/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift +++ b/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift @@ -338,12 +338,7 @@ struct SelfHostedSiteAuthenticator { if let options = responseObject as? [AnyHashable: Any] { continuation.resume(returning: options) } else { - let error = NSError( - domain: "WordPressOrgXMLRPCApiErrorDomain", - code: 7, // responseSerializationFailed - userInfo: [NSLocalizedDescriptionKey: "Unable to read the WordPress site at that URL."] - ) - continuation.resume(throwing: error) + continuation.resume(throwing: WordPressOrgXMLRPCApiError.responseSerializationFailed) } } failure: { error, _ in continuation.resume(throwing: error) diff --git a/WordPress/Classes/Login/WordPressDotComAuthenticator.swift b/WordPress/Classes/Login/WordPressDotComAuthenticator.swift index 571a37714f23..703a3299e362 100644 --- a/WordPress/Classes/Login/WordPressDotComAuthenticator.swift +++ b/WordPress/Classes/Login/WordPressDotComAuthenticator.swift @@ -52,7 +52,9 @@ struct WordPressDotComAuthenticator { case loadingSites(Error) } - private static let callbackNotification = Foundation.Notification.Name(rawValue: "WordPressDotComAuthenticatorCallbackURL") + private static let callbackNotification = Foundation.Notification.Name( + rawValue: "WordPressDotComAuthenticatorCallbackURL" + ) static func redirectURI(for scheme: String) -> String { "\(scheme)://oauth2-callback" @@ -96,7 +98,10 @@ struct WordPressDotComAuthenticator { /// - Parameters: /// - email: When provided, the signed-in account must be the account with the given email address. @MainActor - func signIn(from viewController: UIViewController, context: SignInContext) async -> TaggedManagedObjectID? { + func signIn( + from viewController: UIViewController, + context: SignInContext + ) async -> TaggedManagedObjectID? { WPAnalytics.track(.wpcomWebSignIn, properties: ["stage": "start"]) do { let account = try await attemptSignIn(from: viewController, context: context) @@ -116,7 +121,10 @@ struct WordPressDotComAuthenticator { /// - Parameters: /// - email: When provided, the signed-in account must be the account with the given email address. @MainActor - func attemptSignIn(from viewController: UIViewController, context: SignInContext) async throws(SignInError) -> TaggedManagedObjectID { + func attemptSignIn( + from viewController: UIViewController, + context: SignInContext + ) async throws(SignInError) -> TaggedManagedObjectID { let defaultAccount = try? WPAccount.lookupDefaultWordPressComAccount(in: coreDataStack.mainContext) let hasAlreadySignedIn = defaultAccount != nil @@ -125,7 +133,11 @@ struct WordPressDotComAuthenticator { if let tokenLaunchArgument = UserDefaults.standard.string(forKey: "ui-test-wpcom-token") { token = tokenLaunchArgument } else { - token = try await authenticate(from: viewController, prefersEphemeralWebBrowserSession: hasAlreadySignedIn, accountEmail: context.accountEmail(in: coreDataStack.mainContext)) + token = try await authenticate( + from: viewController, + prefersEphemeralWebBrowserSession: hasAlreadySignedIn, + accountEmail: context.accountEmail(in: coreDataStack.mainContext) + ) } } catch { throw .authentication(error) @@ -143,9 +155,17 @@ struct WordPressDotComAuthenticator { // Fetch WP.com account details let user: RemoteUser do { - let service = AccountServiceRemoteREST(wordPressComRestApi: WordPressComRestApi.defaultApi(oAuthToken: token, userAgent: WPUserAgent.wordPress())) + let service = AccountServiceRemoteREST( + wordPressComRestApi: WordPressComRestApi.defaultApi( + oAuthToken: token, + userAgent: WPUserAgent.wordPress() + ) + ) user = try await withCheckedThrowingContinuation { continuation in - service.getAccountDetails(success: { continuation.resume(returning: $0!) }, failure: { continuation.resume(throwing: $0!) }) + service.getAccountDetails( + success: { continuation.resume(returning: $0!) }, + failure: { continuation.resume(throwing: $0!) } + ) } } catch { throw .fetchUser(error) @@ -178,10 +198,12 @@ struct WordPressDotComAuthenticator { } // Post a notification if the current signed-in account is set as the default account. - // This sending notification code exists because that's what the existing login system does. We can consider - // removing this notification once WordPressAuthenticator is removed. + // Several parts of the app (My Sites, widgets, shortcuts, sidebar) observe this + // notification to react to a completed sign-in. if case .default = context { - let notification = Foundation.Notification.Name(rawValue: WordPressAuthenticationManager.WPSigninDidFinishNotification) + let notification = Foundation.Notification.Name( + rawValue: WordPressAuthenticationManager.WPSigninDidFinishNotification + ) let newAccount = try? coreDataStack.mainContext.existingObject(with: accountID) NotificationCenter.default.post(name: notification, object: newAccount) } @@ -218,7 +240,7 @@ struct WordPressDotComAuthenticator { "client_id": clientId, "redirect_uri": redirectURI, "response_type": "code", - "scope": "global", + "scope": "global" ] if let accountEmail { queries["user_email"] = accountEmail @@ -227,16 +249,31 @@ struct WordPressDotComAuthenticator { // Using Alamofire instead of URL to encode query string because URL do not encoded "+" (which may present // in user's email) in query. WP.com treat "+" in URL query as a whitespace, which cause the login page to // prepopulate the email address incorrectly, i.e. "foo+bar@baz.com" shows as "foo bar@baz.com" - let authorizeURL = try? URLEncoding.queryString.encode(URLRequest(url: URL(string: "https://public-api.wordpress.com/oauth2/authorize")!), with: queries).url + let authorizeURL = try? URLEncoding.queryString + .encode(URLRequest(url: URL(string: "https://public-api.wordpress.com/oauth2/authorize")!), with: queries) + .url guard let authorizeURL else { throw .urlError(URLError(.badURL)) } - let callbackURL = try await authorize(from: viewController, url: authorizeURL, prefersEphemeralWebBrowserSession: prefersEphemeralWebBrowserSession, redirectURI: redirectURI) + let callbackURL = try await authorize( + from: viewController, + url: authorizeURL, + prefersEphemeralWebBrowserSession: prefersEphemeralWebBrowserSession, + redirectURI: redirectURI + ) do { - return try await handleAuthorizeCallbackURL(callbackURL, clientId: clientId, clientSecret: clientSecret, redirectURI: redirectURI) + return try await handleAuthorizeCallbackURL( + callbackURL, + clientId: clientId, + clientSecret: clientSecret, + redirectURI: redirectURI + ) } catch { if case .loginDenied = error, recoverDenyAccess { - return try await self.recoverLoginDeniedError(viewController: viewController, accountEmail: accountEmail) + return try await self.recoverLoginDeniedError( + viewController: viewController, + accountEmail: accountEmail + ) } else { throw error } @@ -244,7 +281,12 @@ struct WordPressDotComAuthenticator { } @MainActor - private func authorize(from viewController: UIViewController, url authorizeURL: URL, prefersEphemeralWebBrowserSession: Bool, redirectURI: String) async throws(AuthenticationError) -> URL { + private func authorize( + from viewController: UIViewController, + url authorizeURL: URL, + prefersEphemeralWebBrowserSession: Bool, + redirectURI: String + ) async throws(AuthenticationError) -> URL { if let authenticator { return try authenticator(authorizeURL) } @@ -279,7 +321,9 @@ struct WordPressDotComAuthenticator { let callbackURLViaWebAuthenticationSession = PassthroughSubject() let provider = WebAuthenticationPresentationAnchorProvider(anchor: viewController.view.window ?? UIWindow()) - let session = ASWebAuthenticationSession(url: authorizeURL, callbackURLScheme: redirectURIScheme) { url, error in + let session = ASWebAuthenticationSession(url: authorizeURL, callbackURLScheme: redirectURIScheme) { + url, + error in if let url { callbackURLViaWebAuthenticationSession.send(url) callbackURLViaWebAuthenticationSession.send(completion: .finished) @@ -295,17 +339,21 @@ struct WordPressDotComAuthenticator { do { return try await withTaskCancellationHandler { try await withCheckedThrowingContinuation { continuation in - cancellable.cancellable = Publishers.Merge(callbackURLViaOpenAppURL, callbackURLViaWebAuthenticationSession) + cancellable.cancellable = + Publishers.Merge(callbackURLViaOpenAppURL, callbackURLViaWebAuthenticationSession) .first() - .sink(receiveCompletion: { [session] in - session.cancel() - - if case let .failure(error) = $0 { - continuation.resume(throwing: error) + .sink( + receiveCompletion: { [session] in + session.cancel() + + if case let .failure(error) = $0 { + continuation.resume(throwing: error) + } + }, + receiveValue: { url in + continuation.resume(returning: url) } - }, receiveValue: { url in - continuation.resume(returning: url) - }) + ) } } onCancel: { [session] in session.cancel() @@ -347,7 +395,7 @@ struct WordPressDotComAuthenticator { "client_id": clientId, "client_secret": clientSecret, "redirect_uri": redirectURI, - "code": code, + "code": code ] do { @@ -381,7 +429,10 @@ struct WordPressDotComAuthenticator { } // Present an alert to ask the user to re-authenticate after they tap the "Deny" button. - private func recoverLoginDeniedError(viewController: UIViewController, accountEmail: String?) async throws(AuthenticationError) -> String { + private func recoverLoginDeniedError( + viewController: UIViewController, + accountEmail: String? + ) async throws(AuthenticationError) -> String { let reLogin = await withCheckedContinuation { continuation in DispatchQueue.main.async { let alert = UIAlertController( @@ -389,12 +440,16 @@ struct WordPressDotComAuthenticator { message: Strings.loginDeniedAlertMessage(), preferredStyle: .alert ) - alert.addAction(UIAlertAction(title: SharedStrings.Button.close, style: .cancel) { _ in - continuation.resume(returning: false) - }) - alert.addAction(UIAlertAction(title: Strings.useDifferentAccount, style: .default) { _ in - continuation.resume(returning: true) - }) + alert.addAction( + UIAlertAction(title: SharedStrings.Button.close, style: .cancel) { _ in + continuation.resume(returning: false) + } + ) + alert.addAction( + UIAlertAction(title: Strings.useDifferentAccount, style: .default) { _ in + continuation.resume(returning: true) + } + ) viewController.present(alert, animated: true) } } @@ -404,7 +459,12 @@ struct WordPressDotComAuthenticator { } // Use an ephemeral session here to ignore the existing account in Safari and allow user to sign in with whatever account they'd like to use. - return try await self.authenticate(from: viewController, prefersEphemeralWebBrowserSession: true, accountEmail: accountEmail, recoverDenyAccess: false) + return try await self.authenticate( + from: viewController, + prefersEphemeralWebBrowserSession: true, + accountEmail: accountEmail, + recoverDenyAccess: false + ) } } @@ -448,14 +508,48 @@ private extension WordPressDotComAuthenticator.AuthenticationError { } private enum Strings { - static let accessDenied = NSLocalizedString("wpComLogin.error.accessDenied", value: "Access denied. You need to approve to log in to WordPress.com", comment: "Error message when user denies access to WordPress.com") - static let loginDeniedTitle = NSLocalizedString("wpComLogin.loginDenied.title", value: "Login Cancelled", comment: "Title of alert shown when user cancels WordPress.com login") - static let loginDeniedMessage = NSLocalizedString("wpComLogin.loginDenied.message", value: "You can sign in with a different account if you need a different one. Tap \"%@\" to start.", comment: "Message shown when user denies WordPress.com login, offering option to try with different account") - static let useDifferentAccount = NSLocalizedString("wpComLogin.loginDenied.useDifferentAccount", value: "Use Different Account", comment: "Button title for signing in with a different WordPress.com account") - static let fetchUserError = NSLocalizedString("wpComLogin.error.fetchUser", value: "Failed to load user details", comment: "Error message when failing to load user details during WordPress.com login") - static let mismatchedEmail = NSLocalizedString("wpComLogin.error.mismatchedEmail", value: "Please sign in with email address %@", comment: "Error message when user signs in with an unexpected email address. The first argument is the expected email address") - static let alreadySignedIn = NSLocalizedString("wpComLogin.error.alreadySignedIn", value: "You have already signed in with email address %@. Please sign out try again.", comment: "Error message when user signs in with an different account than the account that's alredy signed in. The first argument is the current signed-in account email address") - static let loadingSitesError = NSLocalizedString("wpComLogin.error.loadingSites", value: "Your account's sites cannot be loaded. Please try again later.", comment: "Error message when failing to load account's site after signing in") + static let accessDenied = NSLocalizedString( + "wpComLogin.error.accessDenied", + value: "Access denied. You need to approve to log in to WordPress.com", + comment: "Error message when user denies access to WordPress.com" + ) + static let loginDeniedTitle = NSLocalizedString( + "wpComLogin.loginDenied.title", + value: "Login Cancelled", + comment: "Title of alert shown when user cancels WordPress.com login" + ) + static let loginDeniedMessage = NSLocalizedString( + "wpComLogin.loginDenied.message", + value: "You can sign in with a different account if you need a different one. Tap \"%@\" to start.", + comment: "Message shown when user denies WordPress.com login, offering option to try with different account" + ) + static let useDifferentAccount = NSLocalizedString( + "wpComLogin.loginDenied.useDifferentAccount", + value: "Use Different Account", + comment: "Button title for signing in with a different WordPress.com account" + ) + static let fetchUserError = NSLocalizedString( + "wpComLogin.error.fetchUser", + value: "Failed to load user details", + comment: "Error message when failing to load user details during WordPress.com login" + ) + static let mismatchedEmail = NSLocalizedString( + "wpComLogin.error.mismatchedEmail", + value: "Please sign in with email address %@", + comment: + "Error message when user signs in with an unexpected email address. The first argument is the expected email address" + ) + static let alreadySignedIn = NSLocalizedString( + "wpComLogin.error.alreadySignedIn", + value: "You have already signed in with email address %@. Please sign out try again.", + comment: + "Error message when user signs in with an different account than the account that's alredy signed in. The first argument is the current signed-in account email address" + ) + static let loadingSitesError = NSLocalizedString( + "wpComLogin.error.loadingSites", + value: "Your account's sites cannot be loaded. Please try again later.", + comment: "Error message when failing to load account's site after signing in" + ) static func loginDeniedAlertMessage() -> String { String(format: loginDeniedMessage, useDifferentAccount)