Skip to content

Commit

Permalink
Merge pull request #47 from forem/joshpuetz/switch-webview-content-mo…
Browse files Browse the repository at this point in the history
…de-for-third-party-oauth
  • Loading branch information
joshpuetz committed Feb 8, 2022
2 parents afe8bd4 + bfd8577 commit 5dfaa77
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 34 deletions.
9 changes: 9 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@
"version": "4.2.0"
}
},
{
"package": "Fakery",
"repositoryURL": "https://github.com/vadymmarkov/Fakery",
"state": {
"branch": null,
"revision": "71cb3bf36a808534659d1248780c2bf3c4c4fc91",
"version": "5.1.0"
}
},
{
"package": "PryntTrimmerView",
"repositoryURL": "https://github.com/HHK1/PryntTrimmerView",
Expand Down
10 changes: 9 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ let package = Package(
.package(url: "https://github.com/Yummypets/YPImagePicker", .revision("2cf2d150bb0861f2079bc44b56c17aabf5e5d5aa")),
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.4.0"),
.package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.1.0"),
.package(url: "https://github.com/vadymmarkov/Fakery", .upToNextMajor(from: "5.0.0")),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
Expand All @@ -29,6 +30,13 @@ let package = Package(
.product(name: "YPImagePicker", package: "YPImagePicker", condition: .when(platforms: [.iOS])),
],
resources: []),
.testTarget(name: "ForemWebViewTests", dependencies: ["ForemWebView"], resources: [.process("Assets")])
.testTarget(
name: "ForemWebViewTests",
dependencies: [
"ForemWebView",
.product(name: "Fakery", package: "Fakery"),
],
resources: [.process("Assets")]
)
]
)
14 changes: 10 additions & 4 deletions Sources/ForemWebView/ForemWebView+WKNavigationDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,28 @@ extension ForemWebView: WKNavigationDelegate {

public func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Swift.Void) {
preferences: WKWebpagePreferences,
decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Swift.Void) {

guard let url = navigationAction.request.url else {
decisionHandler(.allow)
decisionHandler(.allow, preferences)
return
}
let policy = navigationPolicy(url: url, navigationType: navigationAction.navigationType)

//If we're going off to select OAuth providers, pop us into Desktop mode so we pass user agent checks
if url.isGoogleAuth || url.isFacebookAuth {
preferences.preferredContentMode = .desktop
}

// target="_blank" normal navigation won't work and in order for the webview to follow
// these links (specially within an iframe) requires us to capture the navigation and
// `.cancel` it, then manually loading the URL.
if policy == .allow && navigationAction.targetFrame == nil {
decisionHandler(.cancel)
decisionHandler(.cancel, preferences)
load(url.absoluteString)
} else {
decisionHandler(policy)
decisionHandler(policy, preferences)
}
}

Expand Down
27 changes: 1 addition & 26 deletions Sources/ForemWebView/ForemWebView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,32 +62,7 @@ open class ForemWebView: WKWebView {
}

class func isOAuthUrl(_ url: URL) -> Bool {
// GitHub OAuth paths including 2FA + error pages
if url.absoluteString.hasPrefix("https://github.com/login") ||
url.absoluteString.hasPrefix("https://github.com/session") {
return true
}

// Twitter OAuth paths including error pages
if url.absoluteString.hasPrefix("https://api.twitter.com/oauth") ||
url.absoluteString.hasPrefix("https://twitter.com/login/error") {
return true
}

// Regex for Facebook OAuth based on their API versions
// Example: "https://www.facebook.com/v4.0/dialog/oauth"
let fbRegex = #"https://(www|m)?\.facebook\.com/(v\d+.\d+/dialog/oauth|login.php)"#
if url.absoluteString.range(of: fbRegex, options: .regularExpression) != nil {
return true
}

// Forem Passport Auth
if url.absoluteString.hasPrefix("https://passport.forem.com/oauth") {
return true
}

// Didn't match any supported OAuth URL
return false
url.isOAuthUrl()
}

func setupWebView() {
Expand Down
56 changes: 56 additions & 0 deletions Sources/ForemWebView/URL+ForemUtilities.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Foundation

public extension URL {
// Regex for Facebook OAuth based on their API versions
// Example: "https://www.facebook.com/v4.0/dialog/oauth"
static let faceBookRegex = #"https://(www|m)?\.facebook\.com/(v\d+.\d+/dialog/oauth|login.php)"#
var isFacebookAuth: Bool {
self.absoluteString.range(of: URL.faceBookRegex, options: .regularExpression) != nil
}

// Forem Passport Auth
var isForemPassportAuth: Bool { self.absoluteString.hasPrefix("https://passport.forem.com/oauth") }

// GitHub OAuth paths including 2FA + error pages
var isGithubAuth: Bool {
self.absoluteString.hasPrefix("https://github.com/login") ||
self.absoluteString.hasPrefix("https://github.com/session")
}

//Google OAuth pages
var isGoogleAuth: Bool { self.absoluteString.hasPrefix("https://accounts.google.com") }

// Twitter OAuth paths including error pages
var isTwitterAuth: Bool {
self.absoluteString.hasPrefix("https://api.twitter.com/oauth") ||
self.absoluteString.hasPrefix("https://twitter.com/login/error")
}


func isOAuthUrl() -> Bool {


if isGithubAuth {
return true
}

if isTwitterAuth {
return true
}

if isFacebookAuth {
return true
}

if isGoogleAuth {
return true
}

if isForemPassportAuth {
return true
}

// Didn't match any supported OAuth URL
return false
}
}
119 changes: 119 additions & 0 deletions Tests/ForemWebViewTests/URL+ForemUtilitiesTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import XCTest
import Fakery

class URL_ForemUtilitiesTests: XCTestCase {

static var allTests = [
("testIsOauthURL", testIsOauthURL),
("testIsGoogleAuth", testIsGithubAuth),
("testIsGithubAuth", testIsGoogleAuth),
("testIsFacebookAuth", testIsFacebookAuth),
("testIsForemPassportAuth", testIsForemPassportAuth),
("testIsTwitterAuth", testIsTwitterAuth),
]
static let faker = Faker()

static let githubUrlStrings = [
"https://github.com/login",
"https://github.com/sessions/two-factor",
"""
https://github.com/login?client_id=123123123123&
return_to=%2Flogin%2Foauth%2Fauthorize%3Fclient_id%3Dd7251d40ac9298bdd9fe%26redirect_uri%3D
https%253A%252F%252Fdev.to%252Fusers%252Fauth%252Fgithub%252Fcallback%26response_type%3D
code%26scope%3Duser%253Aemail%26state%3Dfb251bee9df12312312313d6e228bdc63
""",
]

static let twitterUrlStrings = [
"https://api.twitter.com/oauth",
"https://api.twitter.com/oauth/authenticate?oauth_token=-_1DwgA123123123YqVY",
"""
https://twitter.com/login/error?username_or_email=asdasda&redirect_after_login=
https%3A%2F%2Fapi.twitter.com%2Foauth%2Fauthenticate%3Foauth_token%3D-_1DwgAAAAAAa8cGAAABdXEYqVY
""",
]

static let facebookUrlStrings = [
"https://www.facebook.com/v4.0/dialog/oauth",
"https://www.facebook.com/v5.9/dialog/oauth",
"https://www.facebook.com/v6.0/dialog/oauth",
"https://m.facebook.com/v4.0/dialog/oauth",
"https://m.facebook.com/v6.0/dialog/oauth",
"https://m.facebook.com/login.php?skip_api_login=1&api_key=asdf",
]

static let passportUrlStrings = [
"https://passport.forem.com/oauth/authorize?client_id=IBex_ltWo0tiuoB9CgHt7LCrwTuG5rlwhphjzQdf1RA&redirect_uri=https%3A%2F%2Fgggames.visualcosita.com%2Fusers%2Fauth%2Fforem%2Fcallback&response_type=code&state=de3f6b0c4cac41fdb9abf5409ce2f24e2d743245ca37a53a"
]

static let googleUrlStrings = [
"https://accounts.google.com/o/oauth2/v2/auth",
"""
https://accounts.google.com/o/oauth2/v2/auth?
scope=https%3A//www.googleapis.com/auth/drive.metadata.readonly&
access_type=offline&
include_granted_scopes=true&
response_type=code&
state=state_parameter_passthrough_value&
redirect_uri=https%3A//oauth2.example.com/code&
client_id=client_id
""",
]

static let urlStrings = githubUrlStrings + twitterUrlStrings + facebookUrlStrings + passportUrlStrings + googleUrlStrings

func testIsOauthURL() {
for urlString in URL_ForemUtilitiesTests.urlStrings {
if let url = URL(string: urlString) {
XCTAssertTrue(url.isOAuthUrl(), "String didn't match as Auth URL: \(urlString)")
}
}

for _ in 0...5 {
if let url = URL(string: URL_ForemUtilitiesTests.faker.internet.url()) {
XCTAssertFalse(url.isOAuthUrl(), "String incorrectly identified a Auth URL: \(url.absoluteString)")
}
}
}

func testIsGithubAuth() {
for urlString in URL_ForemUtilitiesTests.githubUrlStrings {
if let url = URL(string: urlString) {
XCTAssertTrue(url.isGithubAuth, "String didn't match as Auth URL: \(urlString)")
}
}
}

func testIsGoogleAuth() {
for urlString in URL_ForemUtilitiesTests.googleUrlStrings {
if let url = URL(string: urlString) {
XCTAssertTrue(url.isGoogleAuth, "String didn't match as Auth URL: \(urlString)")
}
}
}

func testIsFacebookAuth() {
for urlString in URL_ForemUtilitiesTests.facebookUrlStrings {
if let url = URL(string: urlString) {
XCTAssertTrue(url.isFacebookAuth, "String didn't match as Auth URL: \(urlString)")
}
}
}

func testIsForemPassportAuth() {
for urlString in URL_ForemUtilitiesTests.passportUrlStrings {
if let url = URL(string: urlString) {
XCTAssertTrue(url.isForemPassportAuth, "String didn't match as Auth URL: \(urlString)")
}
}
}

func testIsTwitterAuth() {
for urlString in URL_ForemUtilitiesTests.twitterUrlStrings {
if let url = URL(string: urlString) {
XCTAssertTrue(url.isTwitterAuth, "String didn't match as Auth URL: \(urlString)")
}
}
}

}
1 change: 1 addition & 0 deletions Tests/ForemWebViewTests/XCTestManifests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import XCTest
public func allTests() -> [XCTestCaseEntry] {
return [
testCase(ForemWebViewTests.allTests),
testCase(URL_ForemUtilitiesTests.allTests),
]
}
#endif
9 changes: 6 additions & 3 deletions docs/ForemWebView-deep-dive.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ It's important to know this is a custom implementation of `WKWebView` and you **

- `load(_ urlString: String)`
- Helper method for simplicity: `webView.load("https://dev.to")`
- `isOAuthUrl(_ url: URL) -> Bool`
- Responds to whether the url provided is one of the supported 3rd party redirect URLs in a OAuth protocol
- Useful if implementing `WKNavigationDelegate` on your own (not recommended)
- `userData`
- Instance of `ForemUserData` when authenticated or `nil` otherwise
- `foremInstance`
Expand All @@ -31,6 +28,12 @@ It's important to know this is a custom implementation of `WKWebView` and you **
- Instead of polling with this function we recommend you register to observe the `userData` variable as you'll react to changes when they become available
- `fetchUserData(completion: @escaping (ForemUserData?) -> Void)`

Extension to `URL`

- `.isOAuthUrl -> Bool`
- Responds to whether the url is one of the supported 3rd party redirect URLs in a OAuth protocol
- Useful if implementing `WKNavigationDelegate` on your own (not recommended)

## Native Podcast Player & Picture in Picture video

In order for your App to take advantage of these native features via the `ForemWebView` you'll need to configure a few things:
Expand Down

0 comments on commit 5dfaa77

Please sign in to comment.